Objective:
- Authenticate with Cognito (configured with serverless.yml below)
- Hit an authenticated endpoint GET /users to trigger a lambda job.
- 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,
};