This is the third post of our ongoing series about using PostGraphile.
In this post, we’ll learn how to authenticate a user with AWS Cognito and use the incoming JWT with our GraphQL client against PostGraphile.
The idea behind this is to leverage PostGraphile support of PostgreSQL’s RLS.
Cognito JWTs
The first think to understand right now is that Cognito delivers several tokens that you may use with PostGraphile. Note that the token headers are not shown here but they are important because they gives us the public key id to be used to verify the token.
- An access_token, for which a sample payload is shown below.
{ "sub": "44b6a4ec-1578-4975-a36c-d51a506c772c", "event_id": "63410683-6ae9-11e8-a1cd-9f7a63602f61", "token_use": "access", "scope": "aws.cognito.signin.user.admin", "auth_time": 1528441126, "iss": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_____0fOqO", "exp": 1528533040, "iat": 1528529440, "jti": "86fffdd5-1847-42f7-a654-30d1c8be6d92", "client_id": "2g77jmh5atk5e2lbjmrbh5mg3r", "username": "44b6a4ec-1578-4975-a36c-d51a__6c772c" }
- An id_token for which a sample payload is shown below.
{ "sub": "44b6a4ec-1578-4975-a36c-d51a506c772c", "email_verified": true, "iss": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_____0fOqO", "phone_number_verified": false, "cognito:username": "44b6a4ec-1578-4975-a36c------06c772c", "aud": "2g77jmh5atk5e2lbjmrbh5mg3r", "event_id": "63410683-6ae9-11e8-a1cd-9f7a63602f61", "token_use": "id", "auth_time": 1528441126, "phone_number": "+33666666666", "exp": 1528533040, "iat": 1528529440, "email": "omatrot@domain.com" }
- A refresh_token that is useless in our case.
PostGraphile JWT machinery
The idea is that we provide, through the graphql client (more on this below), a JWT token to be processed by PostGraphile to:
- Verify the audience.
- Check that the token has not expired
- Validate the signature.
- Extract the user id to be used as PostgreSQL role.
The id_token above seems a good fit because it contains all the information we need.
Configuration
The verification process is done through the jsonwebtoken module. We need to provide the secret, in that case a public key associated with the Cognito User Pool. It is available a the following address, as explained here.
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
Replace {custom} parts with values that suits you and download the Json fragment returned at this address. They may be several keys (one for the access_oken, another for the id_token). Remember, you must choose the key by matching the key id found in the token header.
The trick here is that the public key must be provided in the PEM format. So we first need a one off process of the information available at the link above.
I have found this javascript node module that suited my need. I’ll let as an exercise to the reader the task to convert a jwk to the PEM format. If you’re curious enough, you could take a look at RFC’s 1421 through 1424
So far I have my public key in PEM format, and everything I need to enhance the Postgraphile startup configuration as seen in the previous post.
The next tricky part is to set a multiline parameter value on the command line… Because I’m using a javascript ecosystem file, I could use the ‘`’ character. You’ll find a sample below. I have removed several lines from my public key but you now have the idea.
{ name : 'postgraphile', script : '/home/ec2-user/.nvm/versions/node/v8.11.2/bin/postgraphile', args : `-c postgres://my-pg-rds-instance.eu-west-1.rds.amazonaws.com/mydb --watch --host ec2-ip-v4-public-address.eu-west-1.compute.amazonaws.com --cors --default-role unauth --jwt-secret '-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk8erpQAjLqCJIHMVGbl1 b/NBMHsNHmb0518WBp9oH4CTGwa/xxblNy0P9EMLSqXSZD0gk/ewO6l4NCSofcXL 9BY94TVDaiDOyh7CAvUzDFTjUcNGeiQbdF7bvbTElqh35M3Dxujn7sWBMq7GKqsW LQIDAQAB -----END PUBLIC KEY-----' --jwt-verify-algorithms RS256 --jwt-role username`, }
The jwt related command arguments are:
- –jwt-secret
- –jwt-verify-algorithms
- –jwt-role
As a side note, I encountered a problem with the public key associated to the id_token that I would have used to check the audience as I originally intended. It troubled the pgSettings code somehow:
0|postgrap | Error: Error converting pgSetting: boolean needs to be of type string or number. 0|postgrap | at isPgSettingValid (/home/ec2-user/.nvm/versions/node/v8.11.2/lib/node_modules/postgraphile/build/postgraphile/withPostGraphileContext.js:252:11) 0|postgrap | at getSettingsForPgClientTransaction (/home/ec2-user/.nvm/versions/node/v8.11.2/lib/node_modules/postgraphile/build/postgraphile/withPostGraphileContext.js:182:17) 0|postgrap | at withDefaultPostGraphileContext (/home/ec2-user/.nvm/versions/node/v8.11.2/lib/node_modules/postgraphile/build/postgraphile/withPostGraphileContext.js:28:51) 0|postgrap | at Object.withPostGraphileContext [as default] (/home/ec2-user/.nvm/versions/node/v8.11.2/lib/node_modules/postgraphile/build/postgraphile/withPostGraphileContext.js:98:12) 0|postgrap | at /home/ec2-user/.nvm/versions/node/v8.11.2/lib/node_modules/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:75:49 0|postgrap | at Promise.all.paramsList.map (/home/ec2-user/.nvm/versions/node/v8.11.2/lib/node_modules/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:499:40) 0|postgrap | at Array.map () 0|postgrap | at requestHandler (/home/ec2-user/.nvm/versions/node/v8.11.2/lib/node_modules/postgraphile/build/postgraphile/http/createPostGraphileHttpRequestHandler.js:435:52) 0|postgrap | at 0|postgrap | at process._tickDomainCallback (internal/process/next_tick.js:228:7)
So I temporarily moved to the access_token because the public key is doing fine.
Using Apollo client
The last thing to do is to actually send a query. Here is one way to set up the Apollo Client (apollo-boost) with the access token JWT coming from the AWS Amplify client:
import ApolloClient from "apollo-boost"; import { Auth } from "aws-amplify"; import gql from "graphql-tag"; private onPress = async () => { try { // Get the logged in user const user = await Auth.currentSession(); const client = new ApolloClient({ request: async (operation) => { // setContext function can't be made async. // This is why the user is retreived above operation.setContext(() => { // get the authentication token from local storage if it exists const token = user.accessToken.jwtToken; // return the headers to the context so httpLink can read them return { headers: { authorization: token ? `Bearer ${token}` : "", }, }; }); }, uri: "http://ec2-my-ip-v4-public.eu-west-1.compute.amazonaws.com:5000/graphql", }); const result = await client.query({ query: gql`{ allPasswds(first: 1) { totalCount } }`, }); console.log(result); } catch (e) { console.log(e); } }
And bingo it works, Postgraphile tries to use the username as the current role, which does not exist yet in my database:
0|postgrap | error: role "44b6a4ec-1578-4975-a36c------6c772c" does not exist
In my opinion, Postgraphile could be improved to download the secret itself because the issuer URL is available in the tokens. I’ll talk about that with Benjie Gillam that helped me a lot on this journey. Kudos man.
Thanks for publishing this series. I hope to use Postgraphile in my projects soon. Please write more about it.
LikeLike
Hi, thanks for your feedback. I’ll try to blog about my Postgraphile usage soon. Stay tuned.
LikeLike