AppSync:使用 AWS_IAM auth 时在 $context 中获取用户信息

AppSync: Get user information in $context when using AWS_IAM auth

在 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。 但是,如果您使用 AWS_IAM 连接到 Cognito(必须具有未经身份验证的访问权限),您应该如何获取用户的用户名、电子邮件、子等?使用 AWS_IAM type Auth.

时,我需要访问用户的声明

为了使用户的用户名、电子邮件、子等可以通过 AppSync API 访问,有一个答案:

总而言之,您想将用户池 ID 令牌发送到您的 API(例如 AppSync 或 API 网关)。您的 API 请求已通过 IAM 身份验证。然后您在 Lambda 函数中验证 ID 令牌,现在您拥有经过验证的 IAM 用户和用户池数据。

您想使用 IAM 的 identity.cognitoIdentityId 作为用户 table 的主键。添加 ID 令牌中包含的数据(用户名、电子邮件等)作为属性。

这样您就可以通过您API 使用户的声明可用。现在,例如,您可以将 $ctx.identity.cognitoIdentityId 设置为项目的所有者。然后也许其他用户可以通过 GraphQL 解析器看到所有者的名字。

如果您需要在您的解析器中访问用户的声明,恐怕目前似乎无法实现。我对此提出了一个问题,因为它对授权非常有帮助:Group authorization in AppSync using IAM authentication

在这种情况下,您可以使用 Lambda 作为数据源并从上述用户 table.

检索用户的声明,而不是使用解析器

目前有点困难:)

这是有效的错误答案。我注意到 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 用户的子项(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。

这是我的答案。 appSync 客户端库中存在一个错误,会覆盖所有自定义 headers。这已经被修复了。现在您可以传递自定义 headers,它将一直传递给您的解析器,我将其传递给我的 lambda 函数(再次注意,我使用的是 lambda 数据源,而不是使用 dynamoDB)。

因此,我在客户端附加了已登录的 JWT,并在我的 lambda 函数中对服务器端进行了解码。您需要 cognito 创建的 public 密钥来验证 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;
    }           
}

这比我的其他 "bad" 答案好得多,因为您并不总是查询数据库来获取客户端已有的信息。根据我的经验,速度大约快 3 倍。

如果您使用的是 AWS Amplify,我为解决此问题所做的工作是设置自定义 header username,如 here 所述,如下所示:

Amplify.configure({
 API: {
   graphql_headers: async () => ({
    // 'My-Custom-Header': 'my value'
     username: 'myUsername'
   })
 }
});

然后在我的解析器中,我可以通过以下方式访问 header:

 $context.request.headers.username

正如 AppSync 的文档 here 访问请求 Headers

部分中所解释的

根据 Honkskillets 的回答,我编写了一个 lambda 函数,它将 return 您的用户属性。您只需使用 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 中使用它,并在需要用户属性时添加此功能。我通过使用 $context.request.

从解析器中的 header 获取 JWT 来提供它