无法在具有无服务器和 DynamoDB/Cognito/API 网关的 lambda 策略中使用 ${cognito-identity.amazonaws.com:sub}

Unable to use ${cognito-identity.amazonaws.com:sub} in lambda policy with serverless and DynamoDB/Cognito/API Gateway

Objective:

  1. 使用 Cognito 进行身份验证(配置为下面的 serverless.yml)
  2. 点击经过身份验证的端点 GET /users 以触发 lambda 作业。
  3. 根据 IAM 策略,使用 LeadingKey 条件限制对基于 Cognito 用户 cognito-identity.amazonaws.com:sub 查询的 DynamoDB table 的访问。

问题: 我的策略似乎没有填充认知变量 ${cognito-identity.amazonaws.com:sub}。如果我手动指定 dynamodb:LeadingKeys 一个值,它就可以正常工作。所以看起来我只需要 Cognito 来正确填充子值,我到处都找了,找不到解决方案。

我的 lambda Role/policy(修改了从无服务器生成的版本以具有 Cognito 和 DynamoDB 规则的信任策略):

{
    "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}"
                }
            }
        }
    ]
}

有信任关系:

{
  "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>"
        }
      }
    }
  ]
}

其他设置信息:

{
  "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@example.com"
}
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}

我已经尝试了几乎所有方法来获取策略中的变量 ${cognito-identity.amazonaws.com:sub},但似乎没有任何效果。

有人知道如何解决这个问题吗?或者我可能会丢失的东西。 (如果我遗漏了任何重要信息,我会更新更多信息)。

谢谢!

编辑:(附加信息)

我的登录函数 (lambda + HTTP API) 在下面,我通过 user/password 授权用户,然后调用 CognitoIdentityCredentials 来“注册”我的身份并从池中获取我的 identityId。 (我验证了我正在注册,因为身份池显示了用户)

然后我的登录调用使用 accessToken、idToken、identityId 进行响应。

我所有其他 API 调用都在 Bearer Authorization 调用中使用 idToken 来授权我,但是似乎我的身份池的授权角色没有被假定,它正在使用我的 lambda 角色来执行。

我在这里错过了什么?我认为 Cognito 会处理身份验证池的假定角色,但似乎整个?感谢您的帮助!

我的请求上下文(来自我的登录函数,注意身份对象充满了空值):

 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
    },

我的登录功能

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,
};

我不太清楚您是否已经假定身份(将您的 ID 令牌从用户池交换为 STS 令牌)。

令人困惑的是,cognito-identity.amazonaws.com:sub 解析为 ID 池身份 ID,而不是来自用户池的 ID 令牌中的主题 ID。请参阅本页的注释部分:https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_cognito-bucket.html

要获取身份凭证,请查看https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetCredentialsForIdentity.html

事实证明,如果您将 AWS Gateway 与 Lambda 结合使用,您不能使用这些变量。

您必须直接从客户端应用程序访问 DynamoDB,您已在其中使用 IAM 身份验证(使用 aws amplify 之类的东西)注册了您的身份。

我最终使用 STS 在我的 lambda 函数中承担 Cognito 的组身份验证角色,并完全绕过身份池。