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

后端 未结 5 1532
迷失自我
迷失自我 2020-12-24 15:47

In AppSync, when you use Cognito User Pools as your auth setting your identity you get

identity: 
   { sub: \'bcb5cd53-315a-40df-a41b-1db02a4c1bd9\',
     is         


        
相关标签:
5条回答
  • 2020-12-24 16:06

    Based on Honkskillets answer, I have written a lambda function that will return you the user attributes. You just supply the function with the JWT.

    const jwt = require("jsonwebtoken");
    const jwkToPem = require("jwk-to-pem");
    const request = require("request-promise");
    
    exports.handler = async (event, context) => {
      try {
        const { token } = event;
        const decodedToken = jwt.decode(token, { complete: true });
        const publicJWT = await request(
          `https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`
        );
    
        const keyObject = JSON.parse(publicJWT).keys.find(
          key => key.kid == decodedToken.header.kid
        );
        const pem = jwkToPem(keyObject);
        return {
          statusCode: 200,
          body: jwt.verify(token, pem)
        };
      } catch (error) {
        console.error(error);
        return {
          statusCode: 500,
          body: error.message
        };
      }
    };
    
    

    I use it in Appsync where I create Pipeline resolvers and add this function whenever I need user attributes. I supply the JWT by grabbing it from the header in the resolver using $context.request.

    0 讨论(0)
  • 2020-12-24 16:07

    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.

    0 讨论(0)
  • 2020-12-24 16:13

    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 :)

    0 讨论(0)
  • 2020-12-24 16:17

    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.

    0 讨论(0)
  • 2020-12-24 16:20

    If you are using AWS Amplify, what I did to get around this was to set a custom header username as explained here, like so:

    Amplify.configure({
     API: {
       graphql_headers: async () => ({
        // 'My-Custom-Header': 'my value'
         username: 'myUsername'
       })
     }
    });
    

    then in my resolver I would have access to the header with:

     $context.request.headers.username
    

    As explained by the AppSync's docs here in the section Access Request Headers

    0 讨论(0)
提交回复
热议问题