Franco Berton - April 6th, 2019
Implementing password grant type using Firestore in Apigee
Index
- 1. Implement an OAuthV2 policy for the client identification
- 2. Extract form parameters from the request (username, password, and grant type)
- 3. Read the Firestore’s private key from Key Value Map
- 4. Generate the Firestore JWT need to obtain the Firestore access token
- 5. Get the access token to communicate with Firestore
- 6. Prepare the authentication request
- 7. Send the request to Firestore
- 8. Extract the user data from the response
- 9. Generate apigee access token with the user data
- 10. Result
Prerequisite: The client app must be registered with Apigee Edge to obtain the client ID and client secret keys. Firestore DB must be created in locked mode. See Registering client apps and Firestore quickstart for details.
1. Implement an OAuthV2 policy for the client identification
This step is the first and is the simplest, the policy identifies the accesses of the clients through the validation of the client key and the client secret. If the key/secret are invalid, the policy returns an error to the client.
<?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. Extract the form parameters from the request (username, password, and grant type)
The below policy extracts the user credentials and the grant type from the request of the client and stores them in variables for later use.
<?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. Read the Firestore’s private key from Key Value Map
The communication with Firestore is not simple in locked mode and we need to make some clarifications. Firestore provides many ways to authenticate by OAuth2 protocol, and the service account is the easiest way to authenticate to your Firebase Realtime Database. If you go in this link you can create or select your service account, and then you can download the JSON key that will be look like this:
{
"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"
}
From JSON key, we need to extract the private key attribute and save it into a key/value map (KVM) store.
The last thing we want to do is reading the private key from KVM by this 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. Generate the Firestore JWT need to obtain the Firestore access token
This step provides the policy to generate a JWT with RS256 asymmetric algorithm, that we going to use in the next step to obtain the access token.
The JWT contains this informations as body data:
{
"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
}
Below, this policy will do the magic:
<?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. Get the access token to communicate with Firestore
The communication with firestore is very close: after obtaining our JWT and storing it to firestore-jwt variable, we can prepare the request with a payload like the following one:
{
'method' : 'post',
'payload' : 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=' **+** firestore-jwt
}
Below, we set the payload of the request:
<?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>
and then we send the payload with this 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>
Now we’ve got an access token for our service account and we can query Firestore! So, let’s go to extract the data from the response.
<?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. Prepare the authentication request
The worst is over, and now we can query Firestore in locked mode and prepare the request to validate user credentials with a policy like this:
<?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>
My Firestore has a collection “users” with a document schema like this:
{
username: string,
password: string
}
7 Send the request to Firestore
It’s time to call Firestore to validate user credentials with this policy:
<?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. Extract the user data from the response
If the validation of the user credentials was successful, we will extract the user info (in this case only ’username’) with the aim of attaching them to 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. Generate apigee access token with the user data
The last step is generating the access token to communicate with your application’s api implemented 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. Result
An access token and refresh token are returned to the 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": "*********"
}
That’s all and I hope to help someone!