Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AWS API Gateway Custom Authorizer based on User Groups

I'm attempting to design a system where users are created in my AWS user pool and assigned to one of four user groups. These user groups have roles attached to them which specify the API Calls they are allowed to make. I've created a user for each group and I'm able to successfully log into them in my Android Application. My User Pool is also attached to an Identity Pool for handling Single Sign On with Identity Federation.

The problem is that rather than assuming the Role assigned to the user group, when I log into the user, the role assigned to the user seems to be coming from the Identity Pool rather than their User Group, and as a result they're unable to make the api calls that they should have access to.

I'm attempting to fix this by implementing a Custom Authorizer in Node.js, but the script appears to be running into some problems. Whenever it enters the ValidateToken() method, it fails saying that the token isn't a JWT token.

console.log('Loading function');

var jwt = require('jsonwebtoken'); 
var request = require('request'); 
var jwkToPem = require('jwk-to-pem');

var groupName = 'MY_GROUP_NAME';
var roleName = 'MY_ROLE_NAME';
var policyName = 'MY_POLICY_NAME';
var userPoolId = 'MY_USER_POOL_ID';
var region = 'MY_REGION';
var iss = 'https://cognito-idp.' + region + '.amazonaws.com/' + userPoolId;
var pems;

exports.handler = function(event, context) {
//Download PEM for your UserPool if not already downloaded
if (!pems) {
//Download the JWKs and save it as PEM
request({
   url: iss + '/.well-known/jwks.json',
   json: true
 }, function (error, response, body) {
    if (!error && response.statusCode === 200) {
        pems = {};
        var keys = body['keys'];
        for(var i = 0; i < keys.length; i++) {
            //Convert each key to PEM
            var key_id = keys[i].kid;
            var modulus = keys[i].n;
            var exponent = keys[i].e;
            var key_type = keys[i].kty;
            var jwk = { kty: key_type, n: modulus, e: exponent};
            var pem = jwkToPem(jwk);
            pems[key_id] = pem;
        }
        //Now continue with validating the token
        ValidateToken(pems, event, context);
    } else {
        //Unable to download JWKs, fail the call
        context.fail("error");
    }
});
} else {
    //PEMs are already downloaded, continue with validating the token
    ValidateToken(pems, event, context);
};
};

function ValidateToken(pems, event, context) {

var token = event.authorizationToken;
//Fail if the token is not jwt
var decodedJwt = jwt.decode(token, {complete: true});
if (!decodedJwt) {
    //THIS IS WHERE THE SCRIPT ENDS UP
    console.log("Not a valid JWT token");
    context.fail("Unauthorized - Invalid Token Provided");
    return;
}

//Fail if token is not from your UserPool
if (decodedJwt.payload.iss != iss) {
    console.log("invalid issuer");
    context.fail("Unauthorized - Invalid Issuer Provided");
    return;
}

//Reject the jwt if it's not an 'Access Token'
if (decodedJwt.payload.token_use != 'access') {
    console.log("Not an access token");
    context.fail("Unauthorized - Not an Access Token");
    return;
}

//Get the kid from the token and retrieve corresponding PEM
var kid = decodedJwt.header.kid;
var pem = pems[kid];
if (!pem) {
    console.log('Invalid access token');
    context.fail("Unauthorized - Invalid Access Token Provided");
    return;
}

//Verify the signature of the JWT token to ensure it's really coming from your User Pool
jwt.verify(token, pem, { issuer: iss }, function(err, payload) {
  if(err) {
        console.log(err, err.stack); // an error occurred
        context.fail("Unauthorized - Could not verify token signature");
  } 
  else {
    //Valid token. Generate the API Gateway policy for the user
    //Always generate the policy on value of 'sub' claim and not for 'username' because username is reassignable
    //sub is UUID for a user which is never reassigned to another user.
    var principalId = payload.sub;
    var username = payload.username;

    var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
    var params = {
        UserPoolId: userPoolId, /* ID of the Target User Pool */
        Username: username, /* Provided by event object??? */
        Limit: 0,
        NextToken: ''       //May need actual token value
    };

    cognitoidentityserviceprovider.adminListGroupsForUser(params, function(err, data) {
        if (err){
            console.log(err, err.stack); // an error occurred
            context.fail("Unauthorized - Could not obtain Groups for User");
        } 
        else{
            var groups = data.Groups;
            var numGroups = groups.length;
            var isFound = false;
            for(var i = 0; i < numGroups; i++){
                if(groups[i].GroupName == groupName){
                    isFound = true;
                }
            }

            if(isFound){
                var iam = new AWS.IAM();
                var iamParams = {
                    PolicyName: policyName, /* Name of the Policy in the User Group Role */
                    RoleName: roleName /* Name of the User Group Role */
                };

                iam.getRolePolicy(params, function(err, data) {
                    if (err){
                        console.log(err, err.stack); // an error occurred
                        context.fail("Unauthorized - Could not acquire Policy for User Group Role");
                    } 
                    else {
                        var policy = data.PolicyDocument;
                        context.succeed(policy);        //May need to build policy
                    }
                });
            }
            else{
                context.fail("Unauthorized - Could not find the required User Group under the User");
            }
        }
    });
  }
});
}

Can anybody identify the problem with this script, or perhaps help me identify why the tokens being set aren't valid JWT tokens? The tokens are sent by an Android Application using the AWS Cognito SDK.

EDIT: Upon further investigation, the token retrieved from event.authorizationToken is of the following format (the [VALUE] blocks are to hide potentially sensitive information):

AWS4-HMAC-SHA256 Credential=[VALUE1]/20170329/us-east-1/execute-api/aws4_request, 
SignedHeaders=host;x-amz-date;x-amz-security-token, 
Signature=[VALUE2]
like image 724
John Riley Avatar asked Oct 17 '25 07:10

John Riley


2 Answers

If clients are getting the AWS credentials after login, you can only use AWS_IAM authorization type on the API Gateway Methods. The authorizationToken value you are seeing is the AWS signature generated by the client using the credentials vended by Cognito. It will not be possible for you to validate the AWS signature in a custom authorizer.

Are you following this Cognito blog post? If so, I think you might be confusing the User Group role with the authenticated role selection on the Identity Pool. When you use the federated identities with User Pool provider, your client will get back AWS credentials that have the permissions of the 'Authenticated role' from that section in the Cognito tab in the Identity Pool. In the blog post this would be the 'EngineerRole' set on the Identity Pool.

like image 194
jackko Avatar answered Oct 18 '25 23:10

jackko


I figured it out: This document (specifically the bottom part) says "If you set roles for groups in an Amazon Cognito user pool, those roles are passed through the user's ID token. To use these roles, you must also set Choose role from token for the authenticated role selection for the identity pool."

All it takes is to set the appropriate Trust Policy on each role, adjust the Identity Pool to use "Choose role from token" with the user pool authentication provider, and the proper roles are now being assumed. For others running into this problem, here is my trust policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Federated": "cognito-identity.amazonaws.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:aud": "[IDENTITY_POOL_ID]"
        },
        "ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "authenticated"
        }
      }
    }
  ]
}
like image 29
John Riley Avatar answered Oct 18 '25 22:10

John Riley



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!