AppSync: Get user information in $context when using AWS_IAM auth

Here is my answer. There was a bug in the appSync client library that would overwrite all custom headers. That has since been fixed. Now you can pass down custom headers that will make it all the way to you resolvers, which I pass to my lambda functions (again, note I am using lambda datasourcres and not using dynamoDB).

So I attach my logged in JWT on the client side and, server side in my lambda function, I decode it. You need the public key created by cognito to validate the JWT. (YOU DO NOT NEED A SECRET KEY.) There is a "well known key" url associated with every user pool which I ping the first time my lambda is spun up but, just like my mongoDB connection, it is persisted between lambda calls (at least for a while.)

Here is lambda resolver...

const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');
const request = require('request-promise-native');
const _ = require('lodash')

//ITEMS THAT SHOULD BE PERSISTED BETWEEN LAMBDA EXECUTIONS
let conn = null; //MONGODB CONNECTION
let pem = null;  //PROCESSED JWT PUBLIC KEY FOR OUR COGNITO USER POOL, SAME FOR EVERY USER

exports.graphqlHandler =  async (event, lambdaContext) => {
    // Make sure to add this so you can re-use `conn` between function calls.
    // See https://www.mongodb.com/blog/post/serverless-development-with-nodejs-aws-lambda-mongodb-atlas
    lambdaContext.callbackWaitsForEmptyEventLoop = false; 

    try{
        ////////////////// AUTHORIZATION/USER INFO /////////////////////////
        //ADD USER INFO, IF A LOGGED IN USER WITH VALID JWT MAKES THE REQUEST
        var token = _.get(event,'context.request.headers.jwt'); //equivalen to "token = event.context.re; quest.headers.alexauthorization;" but fails gracefully
        if(token){
            //GET THE ID OF THE PUBLIC KEY (KID) FROM THE TOKEN HEADER
            var decodedToken = jwt.decode(token, {complete: true});
            // GET THE PUBLIC KEY TO NEEDED TO VERIFY THE SIGNATURE (no private/secret key needed)
            if(!pem){ 
                await request({ //blocking, waits for public key if you don't already have it
                    uri:`https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`,
                    resolveWithFullResponse: true //Otherwise only the responce body would be returned
                })
                    .then(function ( resp) {
                        if(resp.statusCode != 200){
                            throw new Error(resp.statusCode,`Request of JWT key with unexpected statusCode: expecting 200, received ${resp.statusCode}`);
                        }
                        let {body} = resp; //GET THE REPSONCE BODY
                        body = JSON.parse(body);  //body is a string, convert it to JSON
                        // body is an array of more than one JW keys.  User the key id in the JWT header to select the correct key object
                        var keyObject = _.find(body.keys,{"kid":decodedToken.header.kid});
                        pem = jwkToPem(keyObject);//convert jwk to pem
                    });
            }
            //VERIFY THE JWT SIGNATURE. IF THE SIGNATURE IS VALID, THEN ADD THE JWT TO THE IDENTITY OBJECT.
            jwt.verify(token, pem, function(error, decoded) {//not async
                if(error){
                    console.error(error);
                    throw new Error(401,error);
                }
                event.context.identity.user=decoded;
            });
        }
        return run(event)
    } catch (error) {//catch all errors and return them in an orderly manner
        console.error(error);
        throw new Error(error);
    }
};

//async/await keywords used for asynchronous calls to prevent lambda function from returning before mongodb interactions return
async function run(event) {
    // `conn` is in the global scope, Lambda may retain it between function calls thanks to `callbackWaitsForEmptyEventLoop`.
    if (conn == null) {
        //connect asyncoronously to mongodb
        conn = await mongoose.createConnection(process.env.MONGO_URL);
        //define the mongoose Schema
        let mySchema = new mongoose.Schema({ 
            ///my mongoose schem
        }); 
        mySchema('toJSON', { virtuals: true }); //will include both id and _id
        conn.model('mySchema', mySchema );  
    }
    //Get the mongoose Model from the Schema
    let mod = conn.model('mySchema');
    switch(event.field) {
        case "getOne": {
            return mod.findById(event.context.arguments.id);
        }   break;
        case "getAll": {
            return mod.find()
        }   break;
        default: {
            throw new Error ("Lambda handler error: Unknown field, unable to resolve " + event.field);
        }   break;
    }           
}

This is WAY better than my other "bad" answer because you are not always querying a DB to get info that you already have on the client side. About 3x faster in my experience.


For making User's username, email, sub etc. accessible through AppSync API, there's an answer for that: https://stackoverflow.com/a/42405528/1207523

To sum it up, you want to send User Pools ID token to your API (e.g. AppSync or API Gateway). Your API request is IAM authenticated. Then you validate the ID token in a Lambda function and now you have your validated IAM user and User Pools data together.

You want to use the IAM's identity.cognitoIdentityId as primary key for you User table. Add the data included in ID token (username, email, etc.) as attributes.

This way you can make user's claims available through you API. Now, for example, you can set $ctx.identity.cognitoIdentityId as the owner of an item. Then maybe other users can see the name of the owner via GraphQL resolvers.

If you need to access the user's claims in your resolver I'm afraid that doesn't seems to be possible at the moment. I have made a question about this as it would be very helpful for authorization: Group authorization in AppSync using IAM authentication

In this case, instead of using a resolver you could use Lambda as a data source and retrieve the user's claims from the above-mentioned User table.

It's all a bit difficult at the moment :)


Here is bad answer that works. I notice that cognitoIdentityAuthProvider: '"cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob","cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob:CognitoSignIn:1a072f08-5c61-4c89-807e-417d22702eb7" contains the Cognito user's sub (the big after CognitoSignIn). You can extract that with a regex and use the aws-sdk to get the user's info from cognito user pool.

///////RETRIEVE THE AUTHENTICATED USER'S INFORMATION//////////
if(event.context.identity.cognitoIdentityAuthType === 'authenticated'){
    let cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
    //Extract the user's sub (ID) from one of the context indentity fields
    //the REGEX in match looks for the strings btwn 'CognitoSignIn:' and '"', which represents the user sub
    let userSub = event.context.identity.cognitoIdentityAuthProvider.match(/CognitoSignIn:(.*?)"/)[1];
    let filter = 'sub = \"'+userSub+'\"'    // string with format = 'sub = \"1a072f08-5c61-4c89-807e-417d22702eb7\"'
    let usersData = await cognitoidentityserviceprovider.listUsers( {Filter:  filter, UserPoolId: "us-west-2_KsyTKrQ2M",Limit: 1}).promise()
    event.context.identity.user=usersData.Users[0]; 

}

It's a bad answer because you are pinging the User Pool database instead of just decoding a JWT.