6

I have a typical AWS setup, using API Gateway with Cognito user pool authentication and integrated with Lambda functions.

It all works fine, but now I need to be able to get the authenticated user id inside Lambda.

I've saw lots of questions/answers about that on SO, but none which helped to get this done. The closest one is this answer which links to this documentation.

From these links above, I understand that I should access some property of "$context.authorizer.claims" on my mapping template to get user id, but I couldn't find a list of the available properties or which one should be used.

I've also tried to use "$context.authorizer.principalId" but this does only return an empty string.

I'm currently using API gateway "Method Request passthrough" mapping template, but have tried many different mapping templates so far.

What am I missing or doing wrong here?

Please let me know if any further information is required.

GCSDC
  • 3,138
  • 3
  • 28
  • 48

3 Answers3

12

I suggest using the Lambda Proxy Integration. In this case, the event your Lambda receives looks like this:

{
...
  "requestContext": {
      "resourceId": "...",
      "authorizer": {
          "claims": {
              "sub": "<COGNITO SUB>",
              "iss": "...",
              "cognito:username": "<COGNITO USERNAME>",
              "aud": "...",
              "token_use": "id",
              "auth_time": "...",
              "exp": "...",
              "iat": "..."
              ...
          }
      },
      ...
  }
  ...
}

The sub is located at event.requestContext.authorizer.claims.sub and the username at event.requestContext.authorizer.claims['cognito:username'].

If using a mapping template, you can do:

{
  "sub": "$context.authorizer.claims.sub",
  "username": "$context.authorizer.claims["cognito:username"]"
}
jogold
  • 6,667
  • 23
  • 41
  • 2
    Thanks for your answer! I've tested it using proxy integration and it worked fine. Also, as I didn't want to use proxy integration, based on your answer I was able to do it using the following mapping template: `{ "username": "$context.authorizer.claims["cognito:username"]" }`. Please update your answer to also include that and I will accept it. – GCSDC May 01 '19 at 20:22
  • single or double quotes for `"cognito:username"`? – jogold May 01 '19 at 20:26
  • 1
    I'm using it with double quotes and it is working fine. – GCSDC May 01 '19 at 20:32
  • OK, answer updated with info on how to do it with a mapping template. – jogold May 01 '19 at 20:34
  • Proxy integration doesn't work when invoking lambdas asynchronously with the API Gateway, the only way to do go around it is to add a mapping template and. – RnD Apr 22 '22 at 09:25
8

For those who face this issue. "$context.authorizer.*" will always return empty until you are testing it with AWS Api Gateway / Lambda test. Just test it with external tools like Postman and it will be populated.

Ihor Pavlyk
  • 1,111
  • 13
  • 10
  • I don't know why this answer doesn't receive more ups. I had the same issue when using the native API Gateway test feature. Once i used curl, the fields are populated. Btw do you have any reference? Wondering why this behaviour is not documented. – aldred Sep 29 '20 at 01:33
  • 1
    In 2022 and this is still an issue. Wish I found this an hour ago. Thank you. – Presley Cobb Aug 27 '22 at 23:50
0

I struggled to find an answer to this problem for a while because there just aren't any concise responses on any of these threads online.

It sounds like you're trying to come up with an effective Authorization strategy after the user has Authenticated their credentials against your Cognito User Pool using custom attributes.

I created a library that I use to export a few functions that allow me to capture the UserPoolId and the Username for the authenticated user so that I can capture the custom:<attribute> I need within my lambda so that the conditions I have implemented can then consume the API to the remaining AWS Services I need to provide authorization to for each user that is authenticated by my app.

Here is My library:

import AWS from "aws-sdk";
// ensure correct AWS region is set
AWS.config.update({
    region: "us-east-2"
});

// function will parse the user pool id from a string
export function parseUserPoolId(str) {
    let regex = /[^[/]+(?=,)/g;
    let match = regex.exec(str)[0].toString();

    console.log("Here is the user pool id: ", match);

    return match.toString();
}

// function will parse the username from a string
export function parseUserName(str) {
    let regex = /[a-z,A-Z,0-9,-]+(?![^:]*:)/g;
    let match = regex.exec(str)[0].toString();

    console.log("Here is the username: ", match);

    return match.toString();
}

// function retries UserAttributes array from cognito
export function getCustomUserAttributes(upid, un) {
    // instantiate the cognito IdP
    const cognito = new AWS.CognitoIdentityServiceProvider({
        apiVersion: "2016-04-18"
    });

    const params = {
        UserPoolId: upid,
        Username: un
    };

    console.log("UserPoolId....: ", params.UserPoolId);
    console.log("Username....: ", params.Username);

    try {
        const getUser = cognito.adminGetUser(params).promise();
        console.log("GET USER....: ", getUser);
        // return all of the attributes from cognito
        return getUser;
    } catch (err) {
        console.log("ERROR in getCustomUserAttributes....: ", err.message);
        return err;
    }
}

With this library implemented it can now be used by any lambda you need to create an authorization strategy for.

Inside of your lambda, you need to import the library above (I have left out the import statements below, you will need to add those so you can access the exported functions), and you can implement their use as such::

export async function main(event, context) {
  const upId = parseUserPoolId(
    event.requestContext.identity.cognitoAuthenticationProvider
  );
  // Step 2 --> Get the UserName from the requestContext
  const usrnm = parseUserName(
    event.requestContext.identity.cognitoAuthenticationProvider
  );
  // Request body is passed to a json encoded string in
  // the 'event.body'
  const data = JSON.parse(event.body);

  try {
    // TODO: Make separate lambda for AUTHORIZATION
    let res = await getCustomUserAttributes(upId, usrnm);

    console.log("THIS IS THE custom:primaryAccountId: ", res.UserAttributes[4].Value);
    console.log("THIS IS THE custom:ROLE: ", res.UserAttributes[3].Value);
    console.log("THIS IS THE custom:userName: ", res.UserAttributes[1].Value);

    const primaryAccountId = res.UserAttributes[4].Value;

  } catch (err) {
    // eslint-disable-next-line
    console.log("This call failed to getattributes");
    return failure({
      status: false
    });
  }
}


The response from Cognito will provide an array with the custom attributes you need. Console.log the response from Cognito with console.log("THIS IS THE Cognito response: ", res.UserAttributes); and check the index numbers for the attributes you want in your CloudWatch logs and adjust the index needed with:

res.UserAttributes[n]

Now you have an authorization mechanism that you can use with different conditions within your lambda to permit the user to POST to DynamoDB, or use any other AWS Services from your app with the correct authorization for each authenticated user.

lopezdp
  • 1,538
  • 3
  • 21
  • 38