AppSync:在使用AWS_IAM身份验证时,获取$context中的用户信息

20

在 AppSync 中,当您将 Cognito 用户池用作身份验证设置时,您会得到:

identity: 
   { sub: 'bcb5cd53-315a-40df-a41b-1db02a4c1bd9',
     issuer: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_oicu812',
     username: 'skillet',
     claims: 
      { sub: 'bcb5cd53-315a-40df-a41b-1db02a4c1bd9',
        aud: '7re1oap5fhm3ngpje9r81vgpoe',
        email_verified: true,
        event_id: 'bb65ba5d-4689-11e8-bee7-2d0da8da81ab',
        token_use: 'id',
        auth_time: 1524441800,
        iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_oicu812',
        'cognito:username': 'skillet',
        exp: 1524459387,
        iat: 1524455787,
        email: 'myemail@nope.com' },
     sourceIp: [ '11.222.33.200' ],
     defaultAuthStrategy: 'ALLOW',
     groups: null }

然而,当您使用AWS_IAM身份验证时,会得到

identity:
{ accountId: '12121212121', //<--- my amazon account ID
  cognitoIdentityPoolId: 'us-west-2:39b1f3e4-330e-40f6-b738-266682302b59',
  cognitoIdentityId: 'us-west-2:a458498b-b1ac-46c1-9c5e-bf932bad0d95',
  sourceIp: [ '33.222.11.200' ],
  username: 'AROAJGBZT5A433EVW6O3Q:CognitoIdentityCredentials',
  userArn: 'arn:aws:sts::454227793445:assumed-role/MEMORYCARDS-CognitoAuthorizedRole-dev/CognitoIdentityCredentials',
  cognitoIdentityAuthType: 'authenticated',
  cognitoIdentityAuthProvider: '"cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob","cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob:CognitoSignIn:1a072f08-5c61-4c89-807e-417d22702eb7"' }

文档说明了这是预期现象,https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html 。 然而,如果您使用连接到Cognito的AWS_IAM(需要未经身份验证的访问),那么您应该如何获取用户的用户名、电子邮件、sub等信息?在使用AWS_IAM类型的身份验证时,我需要访问用户的声明。
5个回答

7
为了使用户的用户名、电子邮件、订阅等内容通过AppSync API可访问,有一个解决方案:https://dev59.com/_VgQ5IYBdhLWcg3wr13V#42405528 简而言之,您需要将用户池ID令牌发送到API(例如AppSync或API Gateway)。您的API请求经过IAM身份验证。然后您在Lambda函数中验证ID令牌,现在您已经将验证后的IAM用户和用户池数据结合在一起。
您想使用IAM的identity.cognitoIdentityId作为用户表的主键。添加ID令牌中包含的数据(用户名、电子邮件等)作为属性。
这样,您就可以通过API提供用户的声明。例如,您可以将$ctx.identity.cognitoIdentityId设置为项目的所有者。然后,其他用户可以通过GraphQL解析器查看所有者的名称。
如果您需要在解析器中访问用户的声明,恐怕目前似乎不可能。我提出了一个关于此问题的问题,因为这对于授权非常有帮助:Group authorization in AppSync using IAM authentication 在这种情况下,您可以使用Lambda作为数据源,并从上述用户表中检索用户的声明。
目前这些都有点困难 :)

那么当您发送UserPoolID令牌时,是将其作为标头发送的吗?我无法弄清楚如何在使用AWS Amplify与AppSync时附加自定义标头。 - honkskillet
@honkskillet 这里的重点是拥有一个API方法(例如称为syncUser),专门用于将UserPool ID令牌保存到您的数据库中。因此,我会将ID令牌作为此API方法的唯一参数。例如,当用户登录时,您可以进行此API调用。 - Lindlof
好的,我明白了。如果您正在使用DynamoDB,那么这可能是目前唯一可行的解决方法。我正在使用Lambda数据源,因此在VTL模板中访问用户信息并不重要,我只需要在Lambda函数中使用它。我的“错误答案”有一个缺点,即当数据应该在原始请求中时,会产生对数据库的不必要调用。速度较慢。 - honkskillet

5
这是一个能够解决问题的不好的答案。我注意到 cognitoIdentityAuthProvider: '"cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob","cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob:CognitoSignIn:1a072f08-5c61-4c89-807e-417d22702eb7" 包含 Cognito 用户的 sub(在 CognitoSignIn 后面的那个大号码)。你可以使用正则表达式提取它,然后使用 AWS SDK 从 Cognito 用户池中获取用户信息。
///////RETRIEVE THE AUTHENTICATED USER'S INFORMATION//////////
if(event.context.identity.cognitoIdentityAuthType === 'authenticated'){
    let cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
    //Extract the user's sub (ID) from one of the context indentity fields
    //the REGEX in match looks for the strings btwn 'CognitoSignIn:' and '"', which represents the user sub
    let userSub = event.context.identity.cognitoIdentityAuthProvider.match(/CognitoSignIn:(.*?)"/)[1];
    let filter = 'sub = \"'+userSub+'\"'    // string with format = 'sub = \"1a072f08-5c61-4c89-807e-417d22702eb7\"'
    let usersData = await cognitoidentityserviceprovider.listUsers( {Filter:  filter, UserPoolId: "us-west-2_KsyTKrQ2M",Limit: 1}).promise()
    event.context.identity.user=usersData.Users[0]; 

这是一个糟糕的回答,因为你正在ping用户池数据库,而不仅仅是解码JWT。


3
这正是我所要寻找的糟糕答案。 - Zerquix18

3
这是我的答案。appSync客户端库中存在一个错误,会覆盖所有自定义标头。现在已经修复了。现在您可以传递自定义标头,这些标头将一直传递到您的解析器,我将其传递给我的Lambda函数(请注意,我正在使用Lambda数据源,而不是使用dynamoDB)。
因此,我在客户端附加我的登录JWT,在服务器端的Lambda函数中,我对其进行解码。您需要由cognito创建的公钥来验证JWT。(您不需要密钥。)与每个用户池关联的“well known key” URL,我在第一次启动我的Lambda时进行ping,但是,就像我的mongoDB连接一样,在Lambda调用之间保持持久化(至少一段时间)。
以下是Lambda解析器...
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');
const request = require('request-promise-native');
const _ = require('lodash')

//ITEMS THAT SHOULD BE PERSISTED BETWEEN LAMBDA EXECUTIONS
let conn = null; //MONGODB CONNECTION
let pem = null;  //PROCESSED JWT PUBLIC KEY FOR OUR COGNITO USER POOL, SAME FOR EVERY USER

exports.graphqlHandler =  async (event, lambdaContext) => {
    // Make sure to add this so you can re-use `conn` between function calls.
    // See https://www.mongodb.com/blog/post/serverless-development-with-nodejs-aws-lambda-mongodb-atlas
    lambdaContext.callbackWaitsForEmptyEventLoop = false; 

    try{
        ////////////////// AUTHORIZATION/USER INFO /////////////////////////
        //ADD USER INFO, IF A LOGGED IN USER WITH VALID JWT MAKES THE REQUEST
        var token = _.get(event,'context.request.headers.jwt'); //equivalen to "token = event.context.re; quest.headers.alexauthorization;" but fails gracefully
        if(token){
            //GET THE ID OF THE PUBLIC KEY (KID) FROM THE TOKEN HEADER
            var decodedToken = jwt.decode(token, {complete: true});
            // GET THE PUBLIC KEY TO NEEDED TO VERIFY THE SIGNATURE (no private/secret key needed)
            if(!pem){ 
                await request({ //blocking, waits for public key if you don't already have it
                    uri:`https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`,
                    resolveWithFullResponse: true //Otherwise only the responce body would be returned
                })
                    .then(function ( resp) {
                        if(resp.statusCode != 200){
                            throw new Error(resp.statusCode,`Request of JWT key with unexpected statusCode: expecting 200, received ${resp.statusCode}`);
                        }
                        let {body} = resp; //GET THE REPSONCE BODY
                        body = JSON.parse(body);  //body is a string, convert it to JSON
                        // body is an array of more than one JW keys.  User the key id in the JWT header to select the correct key object
                        var keyObject = _.find(body.keys,{"kid":decodedToken.header.kid});
                        pem = jwkToPem(keyObject);//convert jwk to pem
                    });
            }
            //VERIFY THE JWT SIGNATURE. IF THE SIGNATURE IS VALID, THEN ADD THE JWT TO THE IDENTITY OBJECT.
            jwt.verify(token, pem, function(error, decoded) {//not async
                if(error){
                    console.error(error);
                    throw new Error(401,error);
                }
                event.context.identity.user=decoded;
            });
        }
        return run(event)
    } catch (error) {//catch all errors and return them in an orderly manner
        console.error(error);
        throw new Error(error);
    }
};

//async/await keywords used for asynchronous calls to prevent lambda function from returning before mongodb interactions return
async function run(event) {
    // `conn` is in the global scope, Lambda may retain it between function calls thanks to `callbackWaitsForEmptyEventLoop`.
    if (conn == null) {
        //connect asyncoronously to mongodb
        conn = await mongoose.createConnection(process.env.MONGO_URL);
        //define the mongoose Schema
        let mySchema = new mongoose.Schema({ 
            ///my mongoose schem
        }); 
        mySchema('toJSON', { virtuals: true }); //will include both id and _id
        conn.model('mySchema', mySchema );  
    }
    //Get the mongoose Model from the Schema
    let mod = conn.model('mySchema');
    switch(event.field) {
        case "getOne": {
            return mod.findById(event.context.arguments.id);
        }   break;
        case "getAll": {
            return mod.find()
        }   break;
        default: {
            throw new Error ("Lambda handler error: Unknown field, unable to resolve " + event.field);
        }   break;
    }           
}

这个答案比我的另一个“糟糕的”答案好多了,因为你不需要总是查询数据库来获取客户端已经拥有的信息。在我的经验中,快了大约3倍。


0
如果您正在使用AWS Amplify,我解决这个问题的方法是设置一个自定义头部username,如此处所述:
Amplify.configure({
 API: {
   graphql_headers: async () => ({
    // 'My-Custom-Header': 'my value'
     username: 'myUsername'
   })
 }
});

那么在我的解析器中,我将可以访问标题:

 $context.request.headers.username

根据AppSync文档中访问请求头部分这里所述。

10
我认为每个用户都可以伪造任何他们想要的用户名。这对于用户名可能没有太大问题,但如果您将子级用于访问控制,那么情况就变得非常危险了。 - Philiiiiiipp

0

基于Honkskillets的答案,我编写了一个lambda函数,它将返回用户属性。您只需向该函数提供JWT即可。

const jwt = require("jsonwebtoken");
const jwkToPem = require("jwk-to-pem");
const request = require("request-promise");

exports.handler = async (event, context) => {
  try {
    const { token } = event;
    const decodedToken = jwt.decode(token, { complete: true });
    const publicJWT = await request(
      `https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`
    );

    const keyObject = JSON.parse(publicJWT).keys.find(
      key => key.kid == decodedToken.header.kid
    );
    const pem = jwkToPem(keyObject);
    return {
      statusCode: 200,
      body: jwt.verify(token, pem)
    };
  } catch (error) {
    console.error(error);
    return {
      statusCode: 500,
      body: error.message
    };
  }
};

我在Appsync中使用它,创建Pipeline解析器并在需要用户属性时添加此函数。我通过在解析器中使用$context.request从标头中获取JWT。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接