1
votes

Objective:

  1. Authenticate with Cognito (configured with serverless.yml below)
  2. Hit an authenticated endpoint GET /users to trigger a lambda job.
  3. Based on IAM policy, restrict access to DynamoDB table queried based on the cognito users cognito-identity.amazonaws.com:sub using LeadingKey Condition.

The problem: It does not appear that my policy is populating the cognito variable ${cognito-identity.amazonaws.com:sub}. If I manually specify dynamodb:LeadingKeys with a value, it works just fine. So it appears I just need Cognito to populate the sub value in properly, and I have looked everywhere and cannot find a solution.

My lambda Role/policy (Modified the generated version from serverless to have a trust policy with Cognito and DynamoDB rules):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogStream",
                "logs:CreateLogGroup"
            ],
            "Resource": [
                "arn:aws:logs:us-east-1:xxx:log-group:/aws/lambda/exeampleservice*:*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:us-east-1:xxxx:log-group:/aws/lambda/exampleservice*:*:*"
            ],
            "Effect": "Allow"
        },
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:GetItem",
                "dynamodb:Query"
            ],
            "Resource": "*",
            "Condition": {
                "ForAllValues:StringEquals": {
                    "dynamodb:LeadingKeys": "${cognito-identity.amazonaws.com:sub}"
                }
            }
        }
    ]
}

With a trust relationship:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "cognito-identity.amazonaws.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:aud": "us-east-1:<identity pool id>"
        }
      }
    }
  ]
}

Additional setup information:

  • Using API Gateway with http protocol.
  • Created userPool in serverless.yml below.
  • Setup Cognito Identity Pool(Federated).
  • Created a userPool Group and assigned it my Identity pool ID.
  • Assigned a user in the pool to the Group.
  • Authenticated with Cognito and id and access token shows identity id token:
{
  "sub": "xxxx",
  "cognito:groups": [
    "TestGroup"
  ],
  "email_verified": true,
  "iss": "https://cognito-idp.us-east-1.amazonaws.com/<poolid>",
  "cognito:username": "xxx",
  "cognito:roles": [
    "arn:aws:iam::xxxx:role/Cognito_IdentityPoolAuth_Role"
  ],
  "aud": "xxx",
  "event_id": "xxx",
  "token_use": "id",
  "auth_time": 1595367712,
  "exp": 1595371310,
  "iat": 1595367710,
  "email": "[email protected]"
}
  • My simplified Serverless.yml
org: exampleorg
app: exampleapp
service: exampleservers
provider:
  name: aws
  stage: dev
  runtime: nodejs12.x
  iamManagedPolicies:
    - 'arn:aws:iam::xxxx:policy/UserAccess'
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - { 'Fn::ImportValue': '${self:provider.stage}-UsersTableArn' }
      Condition:
        {
          'ForAllValues:StringEquals':
            { // use join to avoid conflict with serverless variable syntax. Ouputs 
              'dynamodb:LeadingKeys':
                [Fn::Join: ['', ['$', '{cognito-identity.amazonaws.com:sub}']]],
            },
        }

  httpApi:
    authorizers:
      serviceAuthorizer:
        identitySource: $request.header.Authorization
        issuerUrl:
          Fn::Join:
            - ''
            - - 'https://cognito-idp.'
              - '${opt:region, self:provider.region}'
              - '.amazonaws.com/'
              - Ref: serviceUserPool
        audience:
          - Ref: serviceUserPoolClient
functions:
  # auth
  login:
    handler: auth/handler.login
    events:
      - httpApi:
          method: POST
          path: /auth/login
          # authorizer: serviceAuthorizer

  # user
  getProfileInfo:
    handler: user/handler.get
    events:
      - httpApi:
          method: GET
          path: /user/profile
          authorizer: serviceAuthorizer
resources:
  Resources:
    HttpApi:
      DependsOn: serviceUserPool
    serviceUserPool:
      Type: AWS::Cognito::UserPool
      Properties:
        UserPoolName: service-user-pool-${opt:stage, self:provider.stage}
        UsernameAttributes:
          - email
        AutoVerifiedAttributes:
          - email
    serviceUserPoolClient:
      Type: AWS::Cognito::UserPoolClient
      Properties:
        ClientName: service-user-pool-client-${opt:stage, self:provider.stage}
        AllowedOAuthFlows:
          - implicit
        AllowedOAuthFlowsUserPoolClient: true
        AllowedOAuthScopes:
          - phone
          - email
          - openid
          - profile
          - aws.cognito.signin.user.admin
        UserPoolId:
          Ref: serviceUserPool
        CallbackURLs:
          - https://localhost:3000
        ExplicitAuthFlows:
          - ALLOW_USER_SRP_AUTH
          - ALLOW_REFRESH_TOKEN_AUTH
        GenerateSecret: false
        SupportedIdentityProviders:
          - COGNITO
    serviceUserPoolDomain:
      Type: AWS::Cognito::UserPoolDomain
      Properties:
        UserPoolId:
          Ref: serviceUserPool
        Domain: service-user-pool-domain-${opt:stage, self:provider.stage}-${self:provider.environment.DOMAIN_SUFFIX}

I have tried just about everything to get the variable ${cognito-identity.amazonaws.com:sub} in the policy, but nothing seems to work.

Does anyone have an idea on how to fix this? or what I could be missing. (I will update with more information if I missed anything critical).

Thanks!

Edit: (Additional information)

My login function(lambda + HTTP API) is below, where I authorizeUser via user/password, then call CognitoIdentityCredentials to "register" my identity and get my identityId from the pool. (I verified I am registering as the identity pool shows the user)

My login call then responds with with the accessToken, idToken, identityId.

All my other API calls use the idToken in a Bearer Authorization call which authorizes me, however it appears that my Authorized role for my identity pool is not assumed and it is using my lambda role for execution.

What am I missing here? I thought Cognito would handle the assumed role of the Authenticated Identity pool, but it appears that the entire ? Any help is appreciated!

My request context(from my login function, note the identity object is full of null values):

 requestContext: {
    accountId: 'xxx',
    apiId: 'xxx',
    domainName: 'xxxx.execute-api.us-east-1.amazonaws.com',
    domainPrefix: 'xxx',
    extendedRequestId: 'xxxx=',
    httpMethod: 'POST',
    identity: {
      accessKey: null,
      accountId: null,
      caller: null,
      cognitoAuthenticationProvider: null,
      cognitoAuthenticationType: null,
      cognitoIdentityId: null,
      cognitoIdentityPoolId: null,
      principalOrgId: null,
      sourceIp: 'xxxx',
      user: null,
      userAgent: 'PostmanRuntime/7.26.1',
      userArn: null
    },

My login function

const AWS = require('aws-sdk');
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');
global.fetch = require('node-fetch').default; // .default for webpack.
const USER_POOL_ID = process.env.USER_POOL_ID;
const USER_POOL_CLIENT_ID = process.env.USER_POOL_CLIENT_ID;
const USER_POOL_IDENTITY_ID = process.env.USER_POOL_IDENTITY_ID; 
console.log('USER_POOL_ID', USER_POOL_ID);
console.log('USER_POOL_CLIENT_ID', USER_POOL_CLIENT_ID);
console.log('USER_POOL_CLIENT_ID', USER_POOL_IDENTITY_ID);
 
const poolData = {
  UserPoolId: USER_POOL_ID, 
  ClientId: USER_POOL_CLIENT_ID,
};
 
const poolRegion = 'us-east-1';
const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
 
function login(Username, Password) {
  var authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails({
    Username,
    Password,
  });
 
  var userData = {
    Username,
    Pool: userPool,
  };
  var cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
 
  return new Promise((resolve, reject) => {
    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: function (result) {
 
        AWS.config.credentials = new AWS.CognitoIdentityCredentials({
          IdentityPoolId: USER_POOL_IDENTITY_ID, // your identity pool id here
          Logins: {
            // Change the key below according to the specific region your user pool is in.
            [`cognito-idp.${poolRegion}.amazonaws.com/${USER_POOL_ID}`]: result
              .getIdToken()
              .getJwtToken(),
          },
        });
 
        //refreshes credentials using AWS.CognitoIdentity.getCredentialsForIdentity()
        AWS.config.credentials.refresh((error) => {
          if (error) {
            console.error(error);
          } else {
            // Instantiate aws sdk service objects now that the credentials have been updated.
            // example: var s3 = new AWS.S3();
            console.log('Successfully Refreshed!');
            AWS.config.credentials.get(() => {
              // return back all tokens and identityId in login call response body.
              const identityId = AWS.config.credentials.identityId;
              const tokens = {
                accessToken: result.getAccessToken().getJwtToken(),
                idToken: result.getIdToken().getJwtToken(),
                refreshToken: result.getRefreshToken().getToken(),
                identityId,
              };
              resolve(tokens);
            });
          }
        });
      },
      onFailure: (err) => {
        console.log(err);
        reject(err);
      },
    });
  });
}
module.exports = {
  login,
};
2

2 Answers

1
votes

It's not quite clear to me whether you have assumed an identity (exchanged your ID token from the user pool for an STS token).

Confusingly, cognito-identity.amazonaws.com:sub resolves to the ID pool identity ID, not the subject ID in the ID token from the user pool. See the Note section on this page: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_cognito-bucket.html

For getting identity credentials, have a look at https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetCredentialsForIdentity.html

0
votes

It turns out you cannot use these variables if you are using AWS Gateway with Lambda.

You must access DynamoDB directly from the client application where you have registered for your identity using IAM auth(with something like aws amplify).

I ended up using STS to assume the Cognito's group authenticated role in my lambda function, and completely bypassing identity pools.