AppSync:使用 AWS_IAM 身份验证时在 $context 中获取用户信息
Posted
技术标签:
【中文标题】AppSync:使用 AWS_IAM 身份验证时在 $context 中获取用户信息【英文标题】:AppSync: Get user information in $context when using AWS_IAM auth 【发布时间】:2018-10-09 16:07:23 【问题描述】:在 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
(需要未经身份验证的访问),您应该如何获取用户的用户名、电子邮件、子等?使用AWS_IAM
类型身份验证时,我需要访问用户的声明。
【问题讨论】:
【参考方案1】:为了通过 AppSync API 访问用户的用户名、电子邮件、子等,有一个答案:https://***.com/a/42405528/1207523
总而言之,您希望将用户池 ID 令牌发送到您的 API(例如 AppSync 或 API 网关)。您的 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 这里的重点是有一个 API 方法(例如,称为 syncUser),仅用于将 UserPool ID 令牌保存到您的数据库。因此,我将 ID Token 作为此 API 方法中的唯一参数。例如,您可以在用户登录时进行此 API 调用。 好的,我明白了。如果您使用的是 DynamoDB,这可能是当前唯一可行的解决方法。我正在使用 lambda 数据源,因此访问 VTL 模板中的用户信息并不重要。我只需要一个 Lambda 函数。我的“错误答案”具有这种方法的缺点,即当数据应该在原始请求中正确时对数据库进行不必要的调用。慢。【参考方案2】:这是一个有效的坏答案。我注意到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】:这是我的答案。 appSync 客户端库中存在会覆盖所有自定义标头的错误。此后,该问题已得到解决。现在您可以将自定义标头传递给您的解析器,我将其传递给我的 lambda 函数(再次注意,我使用的是 lambda datasourcres,而不是使用 dynamoDB)。
所以我将我登录的 JWT 附加到客户端,并在我的 lambda 函数中的服务器端进行解码。您需要 cognito 创建的公钥来验证 JWT。 (您不需要秘密密钥。)每个用户池都有一个“众所周知的密钥”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 倍。
【讨论】:
【参考方案4】:如果您使用的是 AWS Amplify,我为解决此问题所做的是设置自定义标头 username
,如 here 所述,如下所示:
Amplify.configure(
API:
graphql_headers: async () => (
// 'My-Custom-Header': 'my value'
username: 'myUsername'
)
);
然后在我的解析器中,我可以通过以下方式访问标题:
$context.request.headers.username
正如 AppSync 的文档 here 在访问请求标头部分中所解释的那样
【讨论】:
我认为每个用户都可以假装拥有他们想要的任何用户名。对于用户名来说可能不是很糟糕,但如果您使用 sub 进行访问控制,则会变得非常危险。【参考方案5】:基于 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 中使用它来创建管道解析器并在需要用户属性时添加此功能。我通过使用 $context.request
从解析器的标头中获取 JWT 来提供 JWT。
【讨论】:
以上是关于AppSync:使用 AWS_IAM 身份验证时在 $context 中获取用户信息的主要内容,如果未能解决你的问题,请参考以下文章
使用来自 REST 客户端 Postman 的 AWS_IAM 和 API 密钥对 AWS API 网关进行身份验证
如何使用 python 调用具有 Cognito 身份验证的 AppSync 突变?
在 Graphql 操场上使用 AWS Cognito 用户池对 AppSync 突变进行身份验证
具有公共/私有访问权限的 Appsync 身份验证,无需 AWS Cognito