Franco Berton - 6 Aprile, 2019

Implementazione password grant type attraverso Firestore in Apigee

Index

Pre-requisiti: L'applicazione deve essere registrata con Apigee Edge per ottenere il client ID e il client secret keys. Firestore DB deve essere creato in modalità locked. Vedere Registering client apps e Firestore quickstart per i dettagli.

1. Implementare un OAuthV2 policy per l'identificazione del client

Questo step è il primo ed il più semplice. La policy identifica gli accessi dei client attraverso la validazione della client key e client secret. Se il key/secret non sono valide, la policy ritorna un errore.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OAuthV2 async="false" continueOnError="false" enabled="true" name="OA-VerifyAPIKey-and-Secret">
    <DisplayName>OA-VerifyAPIKey and Secret</DisplayName>
    <ExternalAuthorization>false</ExternalAuthorization>
    <StoreToken>false</StoreToken>
    <Operation>GenerateAccessToken</Operation>
    <!-- This is in millseconds, so expire in an hour -->
    <ExpiresIn>36000000</ExpiresIn>
    <SupportedGrantTypes>
        <GrantType>password</GrantType>
    </SupportedGrantTypes>
    <GrantType>request.formparam.grant_type</GrantType>
    <UserName>request.formparam.username</UserName>
    <PassWord>request.formparam.password</PassWord>
    <GenerateResponse enabled="false"/>
    <GenerateErrorResponse enabled="true"/>
    <Tokens/>
</OAuthV2>

2. Estrarre i **form parameters** dalla richiesta (username, password, and grant type)

Questa policy estrae le credenziali dell'utente e le autorizzazioni dalla richiesta del client, salvandoli in variabili differenti per un secondo uso.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ExtractVariables async="false" continueOnError="false" enabled="true" name="EV-UsernamePassword">
    <DisplayName>EV-UsernamePassword</DisplayName>
    <Source clearPayload="false">request</Source>
    <VariablePrefix/>
    <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
    <FormParam name="grant_type">
        <Pattern ignoreCase="true">{the.grant_type}</Pattern>
    </FormParam>
    <FormParam name="username">
        <Pattern ignoreCase="true">{the.username}</Pattern>
    </FormParam>
    <FormParam name="password">
        <Pattern ignoreCase="true">{the.password}</Pattern>
    </FormParam>
</ExtractVariables>

3. Leggere la chiave privata di Firestore dalla Key Value Map

Ora veniamo alla parte più dura: l'integrazione con Firestore.
La comunicazione con Firestore non è semplice in modalità locked ed è necessario fare delle premesse: Firestore fornisce molte vie di autenticazione attraverso il protocollo OAuth2, la via più semplice per effettuare un'autencazione con Firestore è attraverso i service account.
Accedendo alla Console di Google potrai creare e selezionare il tuo service account e scaricare la JSON KEY che sarà simile alla seguente:

{
    "client_x509_cert_url": "*****",
    "auth_provider_x509_cert_url": "********",
    "token_uri": "********",
    "auth_uri": "********",
    "client_id": "********",
    "client_email": "*******",
    "private_key": "*****************",
    "private_key_id": "********",
    "project_id": "*********",
    "type": "service_account"
}

Dalla JSON key, avremo bisogno di estrarre l'attributo riferito alla private key e salvarlo nella key/value map (KVM) store.
Ora potremo leggere la private key attraverso la KVM attraverso questo policy:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<KeyValueMapOperations async="false" continueOnError="false" enabled="true" name="KVM-GetPrivateKey" mapIdentifier="<namekvmstore>">
    <DisplayName>KVM-GetFirestoreKey</DisplayName>
    <Properties/>
    <ExclusiveCache>false</ExclusiveCache>
    <ExpiryTimeInSecs>300</ExpiryTimeInSecs>
    <Get assignTo="private.key" index="1">
        <Key>
            <Parameter> <namePrivateKey>   </Parameter>
        </Key>
    </Get>
    <Scope>environment</Scope>
</KeyValueMapOperations

4. Generare il Firestore JWT necessario per ottenere l'access token di Firestore

Questo step definisce la policy per generare un JWT con RS256 algoritmo asimmetrico, il quale verrà usato nel prossimo step per ottenere l'access token.

Il Jwt conterrà come "body data" le seguenti informazioni:

{
    "iss" : client_email, *// the *client_email attribute of the service account JSON KEY
    "scope" : "https://www.googleapis.com/auth/datastore",
    "aud" : "https://www.googleapis.com/oauth2/v4/token/",
    "exp" : oneHourFromNowSeconds,
    "iat" : nowSeconds
}

La policy di sequito farà la magia.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<GenerateJWT async="false" continueOnError="false" enabled="true" name="Generate-Firestore-JWT">
    <Algorithm>RS256</Algorithm>
    <PrivateKey>
        <Value ref="private.key"/>
    </PrivateKey>
    <Issuer><email></Issuer>
    <Audience>[https://www.googleapis.com/oauth2/v4/token/](https://www.googleapis.com/oauth2/v4/token/)</Audience>
    <ExpiresIn>1h</ExpiresIn>
    <AdditionalClaims>
        <Claim name="scope">[https://www.googleapis.com/auth/datastore](https://www.googleapis.com/auth/datastore)</Claim>
    </AdditionalClaims>
    <OutputVariable>firestore-jwt</OutputVariable>
</GenerateJWT>

5. Ottenere l'access token per comunicare per Firestore

Una volta ottenuto il nostro JWT e salvato nella variabile firestore-jwt, la comunicazione con firestore è molto vicina. Ora andremo a preparare la richiesta con un "payload" identico a questo:

{
   'method' : 'post',
   'payload' : 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=' **+** firestore-jwt
}

Di seguito definiamo il payload della richiesta:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage async="false" continueOnError="false" enabled="true" name="AM-GetAccesTokenFirestore-1">
    <DisplayName>AM-GetAccesTokenFirestore</DisplayName>
    <Properties/>
    <Set>
        <Headers>
            <Header name="Content-Type">application/x-www-form-urlencoded</Header>
        </Headers>
        <Verb>POST</Verb>
        <Path/>
    </Set>
    <Add>
        <FormParams>
            <FormParam name="grant_type">urn:ietf:params:oauth:grant-type:jwt-bearer</FormParam>
            <FormParam name="assertion">{firestore-jwt}</FormParam>
        </FormParams>
    </Add>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <AssignTo createNew="true" transport="https" type="request">access-token-firestore.request</AssignTo>
</AssignMessage>

e successivamente andiamo a inviarlo come definito da questa policy:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ServiceCallout async="false" continueOnError="false" enabled="true" name="SC-Firestore-Access-Token-1">
    <DisplayName>SC-GetAccessTokenFirestore</DisplayName>
    <Properties/>
    <Request clearPayload="true" variable="access-token-firestore.request">
        <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
    </Request>
    <Response>access-token-firestore.response</Response>
    <HTTPTargetConnection>
        <Properties/>
        <URL>[https://www.googleapis.com/oauth2/v4/token/](https://www.googleapis.com/oauth2/v4/token/)</URL>
    </HTTPTargetConnection>
</ServiceCallout>

Ottenuto l'access token per il nostro service account, possiamo andare ad interrogare Firestore!
Ora, andiamo a estrarre i dati dalla risposta.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ExtractVariables async="false" continueOnError="false" enabled="true" name="EV-FirestoreAccessToken">
    <DisplayName>EV-FirestoreAccessToken</DisplayName>
    <JSONPayload>
        <Variable name="access-token" type="string">
            <JSONPath>$.access_token</JSONPath>
        </Variable>
        <Variable name="token_type" type="string">
            <JSONPath>$.token_type</JSONPath>
        </Variable>
    </JSONPayload>
    <Source clearPayload="false">access-token-firestore.response</Source>
    <VariablePrefix>firestore</VariablePrefix>
</ExtractVariables>

6. Preparare la richiesta di autenticazione

Il peggio è passato, ed ora possiamo iniziare ad interrogare Firestore in modalità locked, preparando la richiesta per validare le credenziali dell'utente con questa policy:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AssignMessage async="false" continueOnError="false" enabled="true" name="AM-CreateAuthenticateRequest">
    <DisplayName>AM-CreateAuthenticateRequest</DisplayName>
    <Properties/>
    <Set>
        <Headers>
            <Header name="Authorization">{firestore.token_type} {firestore.access-token}</Header>
        </Headers>
        <QueryParams>
            <QueryParam name="fields">document/fields</QueryParam>
        </QueryParams>
        <Payload contentType="application/json">
           {
                "structuredQuery": {
                  "where": {
                   "compositeFilter": {
                    "op": "AND",
                    "filters": [
                     {
                      "fieldFilter": {
                       "field": {
                        "fieldPath": "username"
                       },
                       "op": "EQUAL",
                       "value": {
                        "stringValue": "{the.username}"
                       }
                      }
                     },
                     {
                      "fieldFilter": {
                       "field": {
                        "fieldPath": "password"
                       },
                       "op": "EQUAL",
                       "value": {
                        "stringValue": "{the.password}"
                       }
                      }
                     }
                    ]
                   }
                  },
                  "from": [
                   {
                    "collectionId": "users"
                   }
                  ],
                   "limit": 1
                }
            }
        </Payload>
        <Verb>POST</Verb>
        <Path/>
    </Set>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <AssignTo createNew="true" transport="https" type="request">authenticate.request</AssignTo>
</AssignMessage>

Il mio Firestore ha una collection denominata “users” con uno schema come questo:

{
  username: string,
  password: string
}

7. Inviare la richiesta a Firestore

È tempo di chiamare Firestore per validare le credenziali dell'utente:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ServiceCallout async="false" continueOnError="true" enabled="true" name="SC-AuthenticateUser">
    <DisplayName>SC-AuthenticateUser</DisplayName>
    <Properties/>
    <Request clearPayload="false" variable="authenticate.request">
        <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
    </Request>
    <Response>authenticate.response</Response>
    <HTTPTargetConnection>
        <Properties/>
        <URL>[https://firestore.googleapis.com/v1beta1/projects/{projectId}/databases/(default)/documents:runQuery](https://firestore.googleapis.com/v1beta1/projects/apigee-userstore/databases/(default)/documents:runQuery)</URL>
    </HTTPTargetConnection>
</ServiceCallout>

8. Estrarre i dati dell'utente dalla risposta

Se la validazione delle credenziali dell'utente va a buon fine, possiamo estrarre le informazioni dell'utente (in questo caso solo ’username’) con lo scopo di includerlo nell'access token:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ExtractVariables async="false" continueOnError="false" enabled="true" name="EV-ExtractUserInfo">
    <DisplayName>EV-ExtractUserInfo</DisplayName>
    <JSONPayload>
        <Variable name="username" type="string">
            <JSONPath>$[0].document.fields.username.stringValue</JSONPath>
        </Variable>
    <Source clearPayload="false">authenticate.response</Source>
    <VariablePrefix>userinfo</VariablePrefix>
</ExtractVariables>

9. Generare l'access token di apigee con i dati dell'utente

L'ultimo step è generare l'access token per comunicare con le API della vostra applicazione definite in apigee:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OAuthV2 async="false" continueOnError="false" enabled="true" name="OA-GenerateAccessToken-Password">
    <DisplayName>OA-GenerateAccessToken Password</DisplayName>
    <Operation>GenerateAccessToken</Operation>
    <!-- This is in millseconds, so expire in an hour -->
    <ExpiresIn>36000000</ExpiresIn>
    <SupportedGrantTypes>
        <GrantType>password</GrantType>
    </SupportedGrantTypes>
    <Scope>request.queryparam.scope</Scope>
    <GrantType>the.grant_type</GrantType>
    <UserName>the.username</UserName>
    <PassWord>the.password</PassWord>
    <Attributes>
        <Attribute name="username" ref="userinfo.username"/>
    </Attributes>
    <GenerateResponse enabled="true"/>
    <GenerateErrorResponse enabled="true"/>
</OAuthV2>

10 Risultato

L'access token e il refresh token sono ritornate al client.

{
    "refresh_token_expires_in": "0",
    "refresh_token_status": "approved",
    "api_product_list": "[helloworld]",
    "api_product_list_json": [
        "helloworld",
    ],
    "organization_name": "*****",
    "developer.email": "*******",
    "token_type": "Bearer",
    "issued_at": "1523099908121",
    "client_id": "*****",
    "access_token": "yrdSVTO6DAXhbLy9aPlblv1K757p",
    "refresh_token": "SDr836u6pPG7GEguQfSP7fehNGEyq0XE",
    "application_name": "********",
    "scope": "default",
    "refresh_token_issued_at": "1523099908121",
    "expires_in": "35999",
    "refresh_count": "0",
    "status": "approved",
    "username": "*********"
}

Questo è tutto e spero di aver aiutato qualcuno con questo articolo.