[英]Verifying user is authenticated using AWS IOS SDK
I created a lamdba function which does the following: 我创建了一个lamdba函数,它执行以下操作:
var param =
{
IdentityPoolId: "us-east-1:the-full-identity-id",
Logins: {} // To have provider name in a variable
};
param.Logins["com.test.website.login"] = userIdICreatedAndStoredInDynamoDB;
cognitoidentity.getOpenIdTokenForDeveloperIdentity(param,
function(err, data)
{
if (err) return fn(err); // an error occurred
else fn(null, data.IdentityId, data.Token); // successful response
});
It returns the identityId and token for that user. 它返回该用户的identityId和token。 Everything is setup with IAM roles and AWS Cognito Identity and appears to be authenticating in the console.
所有内容都使用IAM角色和AWS Cognito Identity进行设置,并且似乎在控制台中进行身份验证。
I have two questions: 我有两个问题:
Thanks. 谢谢。
To answer the first question: 回答第一个问题:
How do I test in the app that the user is authenticated?
如何在应用程序中测试用户是否经过身份验证? I save the
identityId
and token in the app device.我在应用设备中保存了
identityId
和token。
You test the authentication by making a "Custom Authorizer" 您通过制作“自定义授权程序”来测试身份验证
The AWS example function you can find in the Lambda Example Functions when you go to make a new function (if you filter to NodeJS 4.3 functions, it's towards the back) 当您创建一个新函数时,您可以在Lambda示例函数中找到AWS示例函数(如果您过滤到NodeJS 4.3函数,它将向后移动)
Or you can take a look at THIS which is the same thing, just on GitHub instead. 或者你可以看看这个是同样的东西,只是在GitHub上。
I made a sorta modified version here: 我在这里做了一个sorta修改版本:
"use strict";
const
codes = {
100: "Continue", 101: "Switching Protocols", 102: "Processing",
200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status", 208: "Already Reported", 226: "IM Used",
300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect",
400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long",
415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 418: "I'm a teapot", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 425: "Unordered Collection", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons",
500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 509: "Bandwidth Limit Exceeded", 510: "Not Extended", 511: "Network Authentication Required"
},
resp = ( statusCode, data ) => ( { statusCode, message: codes[ statusCode ], data } ),
AWS = require( "aws-sdk" ),
crypto = require( "crypto" ),
COG = new AWS.CognitoIdentity(),
token = {
algorithm: "aes-256-ctr",
encrypt: item => {
item = JSON.stringify( item );
let cipher = crypto.createCipher( token.algorithm, process.env.PoolId ),
crypted = cipher.update( item, 'utf8', 'base64' );
crypted += cipher.final( 'base64' );
return crypted;
},
decrypt: item => {
let decipher = crypto.createDecipher( token.algorithm, process.env.PoolId ),
dec = decipher.update( item, 'base64', 'utf8' );
dec += decipher.final( 'utf8' );
return dec;
}
};
function AuthPolicy( principal, awsAccountId, apiOptions ) {
this.awsAccountId = awsAccountId;
this.principalId = principal;
this.version = '2012-10-17';
this.pathRegex = new RegExp( '^[/.a-zA-Z0-9-\*]+$' );
this.allowMethods = [];
this.denyMethods = [];
if( !apiOptions || !apiOptions.restApiId ) this.restApiId = '*';
else this.restApiId = apiOptions.restApiId;
if( !apiOptions || !apiOptions.region ) this.region = '*';
else this.region = apiOptions.region;
if( !apiOptions || !apiOptions.stage ) this.stage = '*';
else this.stage = apiOptions.stage;
}
AuthPolicy.HttpVerb = {
GET: 'GET',
POST: 'POST',
PUT: 'PUT',
PATCH: 'PATCH',
HEAD: 'HEAD',
DELETE: 'DELETE',
OPTIONS: 'OPTIONS',
ALL: '*',
};
AuthPolicy.prototype = ( function AuthPolicyClass() {
function addMethod( effect, verb, resource, conditions ) {
if( verb !== '*' && !Object.prototype.hasOwnProperty.call( AuthPolicy.HttpVerb, verb ) ) {
throw new Error( `Invalid HTTP verb ${verb}. Allowed verbs in AuthPolicy.HttpVerb` );
}
if( !this.pathRegex.test( resource ) )
throw new Error( `Invalid resource path: ${resource}. Path should match ${this.pathRegex}` );
let cleanedResource = resource;
if( resource.substring( 0, 1 ) === '/' )
cleanedResource = resource.substring( 1, resource.length );
const resourceArn = `arn:aws:execute-api:${this.region}:${this.awsAccountId}:${this.restApiId}/${this.stage}/${verb}/${cleanedResource}`;
if( effect.toLowerCase() === 'allow' )
this.allowMethods.push( {
resourceArn,
conditions,
} );
else if( effect.toLowerCase() === 'deny' )
this.denyMethods.push( {
resourceArn,
conditions,
} );
}
function getEmptyStatement( effect ) {
const statement = {};
statement.Action = 'execute-api:Invoke';
statement.Effect = effect.substring( 0, 1 ).toUpperCase() + effect.substring( 1, effect.length ).toLowerCase();
statement.Resource = [];
return statement;
}
function getStatementsForEffect( effect, methods ) {
const statements = [];
if( methods.length > 0 ) {
const statement = getEmptyStatement( effect );
for( let i = 0; i < methods.length; i++ ) {
const curMethod = methods[ i ];
if( curMethod.conditions === null || curMethod.conditions.length === 0 )
statement.Resource.push( curMethod.resourceArn );
else {
const conditionalStatement = getEmptyStatement( effect );
conditionalStatement.Resource.push( curMethod.resourceArn );
conditionalStatement.Condition = curMethod.conditions;
statements.push( conditionalStatement );
}
}
if( statement.Resource !== null && statement.Resource.length > 0 )
statements.push( statement );
}
return statements;
}
return {
constructor: AuthPolicy,
allowAllMethods() {
addMethod.call( this, 'allow', '*', '*', null );
},
denyAllMethods() {
addMethod.call( this, 'deny', '*', '*', null );
},
allowMethod( verb, resource ) {
addMethod.call( this, 'allow', verb, resource, null );
},
denyMethod( verb, resource ) {
addMethod.call( this, 'deny', verb, resource, null );
},
allowMethodWithConditions( verb, resource, conditions ) {
addMethod.call( this, 'allow', verb, resource, conditions );
},
denyMethodWithConditions( verb, resource, conditions ) {
addMethod.call( this, 'deny', verb, resource, conditions );
},
build() {
if( ( !this.allowMethods || this.allowMethods.length === 0 ) &&
( !this.denyMethods || this.denyMethods.length === 0 ) )
throw new Error( 'No statements defined for the policy' );
const policy = {}, doc = {};
policy.principalId = this.principalId;
doc.Version = this.version;
doc.Statement = [];
doc.Statement = doc.Statement.concat( getStatementsForEffect.call( this, 'Allow', this.allowMethods ) );
doc.Statement = doc.Statement.concat( getStatementsForEffect.call( this, 'Deny', this.denyMethods ) );
policy.policyDocument = doc;
return policy;
},
};
} () );
exports.handler = ( event, context, cb ) => {
const
principalId = process.env.principalId,
tmp = event.methodArn.split( ':' ),
apiGatewayArnTmp = tmp[ 5 ].split( '/' ),
awsAccountId = tmp[ 4 ],
apiOptions = {
region: tmp[ 3 ],
restApiId: apiGatewayArnTmp[ 0 ],
stage: apiGatewayArnTmp[ 1 ]
},
policy = new AuthPolicy( principalId, awsAccountId, apiOptions );
let response;
if( !event.authorizationToken || typeof event.authorizationToken !== "string" )
response = resp( 401 );
let item = token.decrypt( event.authorizationToken );
try { item = resp( 100, JSON.parse( item ) ); }
catch( e ) { item = resp( 401 ); }
if( item.statusCode !== 100 )
response = resp( 401 );
else if( item.data.Expiration <= new Date().getTime() )
response = resp( 407 );
else
response = resp( 100 );
if( response.statusCode >= 400 ) {
policy.denyAllMethods();
const authResponse = policy.build();
authResponse.context = response;
cb( null, authResponse );
} else {
COG.getCredentialsForIdentity( {
IdentityId: item.data.IdentityId,
Logins: {
'cognito-identity.amazonaws.com': item.data.Token
}
}, ( e, d ) => {
if( e ) {
policy.denyAllMethods();
response = resp( 401 );
} else {
policy.allowMethod( AuthPolicy.HttpVerb.GET, "/user" );
policy.allowMethod( AuthPolicy.HttpVerb.DELETE, "/user" );
response = resp( 202 );
}
const authResponse = policy.build();
authResponse.context = response;
cb( null, authResponse );
} );
}
};
Above is the full example... But let me break this down and explain why the one they provide is not as helpful. 上面是完整的例子......但是让我分解一下并解释为什么他们提供的那个没有帮助。
Here are the steps to setting this up so you can see why it has to be something like this. 以下是设置此步骤的步骤,以便您可以看到为什么它必须是这样的。
Auth_isValid
or something like that Auth_isValid
的函数或类似的东西 PoolId
and principalId
into the Environment Variables so it's easy to change later PoolId
和principalId
放入环境变量中,以便以后更改 Authorizers
Authorizers
Create
-> Custom Authorizer
Create
- > Custom Authorizer
method.request.header.Authorization
for now, and TTL can be 300. Lets not mess with Execution role or token validation expression yet. method.request.header.Authorization
保持简单,TTL可以是300.不要乱用执行角色或令牌验证表达呢。 Ok so when you look at my function, you'll see that I do this weird encrypt/decrypt thing at the very top: 好吧,当你看看我的功能时,你会看到我在最顶端做了这个奇怪的加密/解密事情:
token = {
algorithm: "aes-256-ctr",
encrypt: item => {
item = JSON.stringify( item );
let cipher = crypto.createCipher( token.algorithm, process.env.PoolId ),
crypted = cipher.update( item, 'utf8', 'base64' );
crypted += cipher.final( 'base64' );
return crypted;
},
decrypt: item => {
let decipher = crypto.createDecipher( token.algorithm, process.env.PoolId ),
dec = decipher.update( item, 'base64', 'utf8' );
dec += decipher.final( 'utf8' );
return dec;
}
};
Basically, I wrap some items I want inside an encrypted key simple so I can pass all my information around easy-peasy. 基本上,我将一些我想要的项目包装在一个简单的加密密钥中,这样我就可以轻松地将所有信息传递给我。 (I pass in the Identity Pool as a hash to make it cool and simple and as long as you never send the Identity Pool ID to the front end, we're good!)
(我将身份池作为哈希传递来使其变得很酷且简单,只要你永远不会将身份池ID发送到前端,我们就会很好!)
The Custom Authorizer requires one single token, not a JSON block of what you'll say is a "token" or something (which you could do but it looks dumb) 自定义授权器需要一个令牌,而不是你所说的“令牌”或其他东西的JSON块(你可以做但看起来很愚蠢)
So we have one unified token that gets passed in and I call the decrypt
function for this to unwrap (I'll show the encrypt example in a second. 所以我们有一个统一的令牌传入,我调用
decrypt
函数来解包(我将在一秒钟内显示加密示例)。
Now some people may say "oh well that's not actually encryption it could easily be figured out" - my answer to this is: "ya well it would have been unencrypted, raw text anyway, why not make it easy." 现在有些人可能会说“噢,这实际上不是加密,它很容易被弄清楚” - 我对此的回答是:“好吧,它本来是未加密的,原始文本无论如何,为什么不让它变得容易。”
Ok now that you see that part, head down to the bottom of the function. 好了,现在你看到那个部分,向下到功能的底部。
let response;
if( !event.authorizationToken || typeof event.authorizationToken !== "string" )
response = resp( 401 );
let item = token.decrypt( event.authorizationToken );
try { item = resp( 100, JSON.parse( item ) ); }
catch( e ) { item = resp( 401 ); }
if( item.statusCode !== 100 )
response = resp( 401 );
else if( item.data.Expiration <= new Date().getTime() )
response = resp( 407 );
else
response = resp( 100 );
if( response.statusCode >= 400 ) {
policy.denyAllMethods();
const authResponse = policy.build();
authResponse.context = response;
cb( null, authResponse );
} else {
COG.getCredentialsForIdentity( {
IdentityId: item.data.IdentityId,
Logins: {
'cognito-identity.amazonaws.com': item.data.Token
}
}, ( e, d ) => {
if( e ) {
policy.denyAllMethods();
response = resp( 401 );
} else {
policy.allowMethod( AuthPolicy.HttpVerb.GET, "/user" );
policy.allowMethod( AuthPolicy.HttpVerb.DELETE, "/user" );
response = resp( 202 );
}
const authResponse = policy.build();
authResponse.context = response;
cb( null, authResponse );
} );
}
Update : 更新 :
Our incoming data from API Gateway is:
我们从API Gateway传入的数据是:
{
"type":"TOKEN",
"authorizationToken":"<session_token>",
"methodArn":"arn:aws:execute-api:<region>:<Account_ID>:<API_ID>/<Stage>/<Method>/<Resource_Path>"
}
Our outgoing data from Lambda should be something like:
我们从Lambda传出的数据应该是这样的:
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Deny",
"Resource": [
"arn:aws:execute-api:<region>:<Account_ID>:<API_ID>/<Stage>/*/*"
]
}
]
}
Depending on how our authorization goes.
取决于我们的授权方式。
So in my first if
check, I make sure the authorizationToken
is there and that it's a string
, if it's not, we say it's Unauthorized
(everyone should know and use their status codes) 所以在我的第一次
if
检查中,我确保authorizationToken
在那里并且它是一个string
,如果不是,我们说它是Unauthorized
(每个人都应该知道并使用他们的状态代码)
Second, I decrypt the token and make sure that went well with a try-catch
attempt. 其次,我解密令牌,并确保
try-catch
尝试顺利。 If it didn't go well, they're Unauthorized
. 如果进展不顺利,他们就是
Unauthorized
。 if it did, we can Continue
. 如果确实如此,我们可以
Continue
。
You'll see in the token, I put a variable Expiration
, this is how I check if the key was once accepted and correct and is simply expired now. 你会在令牌中看到我输入一个变量
Expiration
,这就是我如何检查密钥是否曾被接受并且正确并且现在只是过期了。 For this, I say Proxy Authentication Required
. 为此,我说需要
Proxy Authentication Required
。 Which tells my front end, go call login again and give me new creds. 这告诉了我的前端,再次登录并给我新的信誉。 Don't forget, the purpose of this function has to be only to check IF we're authorized.
不要忘记,此功能的目的只是检查我们是否获得授权。 Not to do fancy things like refresh tokens.
不做像刷新令牌这样的花哨的东西。
Next, I check if everything is good and call denyAllMethods
and put the response code in the context
of the response. 接下来,我检查一切是否正常并调用
denyAllMethods
并将响应代码放在响应的context
中。 API Gateway is very picky and only wants simply IAM formatted policies passed around - no other information or format or whatever may be in there if it's not specified HERE or HERE API网关是非常挑剔,只希望通过周围简单地格式化IAM策略- 没有任何其他信息或格式,或者如果它没有指定的任何可能在那里这里或这里
If everything is OK, I call getCredentialsForIdentity
- using the IdentityId
and Token
, make sure that token is, in fact valid as well, and then I allow the functions needed at the time. 如果一切正常,我调用
getCredentialsForIdentity
- 使用IdentityId
和Token
,确保该令牌实际上也是有效的,然后我允许当时所需的功能。 These are very important and will validate the token to only those functions - in other words. 这些非常重要, 只会将令牌验证为那些功能 - 换句话说。 If your IAM role in IAM says it can access everything, this will say no, you can only access
GET
on /user
and DELETE
on /user
. 如果您在IAM IAM角色说,它可以访问所有内容,这也就不多说,你只能访问
GET
对/user
和DELETE
上/user
。 So don't let it fool you. 所以不要让它欺骗你。 This is a custom authorizer after all.
毕竟这是一个自定义授权者 。
Next, I need to show you how I put all this in from the Login part. 接下来,我需要向您展示如何将所有这些放入Login部分。 I have the same
token = {
part but in my login function I added a getToken
function: 我有相同的
token = {
部分但在我的登录功能中我添加了一个getToken
函数:
token.getToken = obj => {
return new Promise( ( res, rej ) => {
COG.getOpenIdTokenForDeveloperIdentity( {
IdentityPoolId: process.env.PoolId,
Logins: {
"com.whatever.developerIdthing": obj.email
},
TokenDuration: duration
}, ( e, r ) => {
r.Expiration = new Date().getTime() + ( duration * 1000 );
if( e ) rej( e );
else res( token.encrypt( r ) );
} );
} );
};
Notice above, the: 上面注意到:
duration
Part. 部分。
This is the answer to your second question: 这是你的第二个问题的答案:
How long does the authentication last?
验证持续多长时间? I want the user to remain logged in. This is how most apps I use work and stays logged in until they hit logout.
我希望用户保持登录状态。这就是我使用的大多数应用程序工作并保持登录状态直到他们注销的方式。
You create an OpenIdToken
using their email or whatever you want to identify them and TokenDuration
is in seconds . 您可以使用他们的电子邮件或任何您想要识别它们的内容创建
OpenIdToken
并且TokenDuration
在几秒钟内完成 。 I would recommend making this a week or two but if you wanted a year long or something, 31536000
would be it. 我建议这一周或两周,但如果你想要一年或者其他东西,
31536000
将是它。 Another way of doing this is to make a function that only gives you authorized credentials, and instead of calling denyAll
in the authorizer when a 407
scenario comes up, make the only method they can call allowMethod( POST, /updateCreds );
另一种方法是创建一个只提供授权凭据的函数,而不是在
407
场景出现时在授权器中调用denyAll
,而是创建唯一可以调用allowMethod( POST, /updateCreds );
or something like that. 或类似的东西。 This way you can refresh their stuff every once in a while.
这样你就可以每隔一段时间刷新一次。
The pseudo for that is: 伪的是:
Remove: 去掉:
if( response.statusCode >= 400 )
else
And do: 并做:
if( statusCode >= 400 )
denyAll
else if( statusCode === 407 )
allow refresh function
else
allow everything else
Hope this helps! 希望这可以帮助!
To test if they're logged in you need to set up a service that'll check the token against Cognito. 要测试他们是否已登录,您需要设置一个服务,以便根据Cognito检查令牌。 Quick and dirty way is to set up a basic lambda, expose it through API Gateway with an authorizer pointed at your User Identity Pool.
快速而肮脏的方法是设置一个基本的lambda,通过API Gateway使用指向您的用户身份池的授权程序公开它。 All the lambda needs to do is return HTTP 200, since what you're really checking is the authorizer.
所有lambda需要做的是返回HTTP 200,因为你真正检查的是授权者。 Then have your app get/post/etc to that API URL w/ a header of "Authorization":$ACCESS_TOKEN.
然后让你的应用程序获取/发布/等到具有“授权”标题的API API:$ ACCESS_TOKEN。 either it'll kick back a 200 on success or it'll return an Unauthorized message.
或者它会在成功时放回200,否则它将返回未经授权的消息。
Your Cognito token is only good for an hour, but you can refresh the token to keep a person logged in. When your user authenticated they got three tokens: ID, Access, and Refresh token. 您的Cognito令牌仅适用于一小时,但您可以刷新令牌以保持用户登录。当您的用户通过身份验证时,他们会获得三个令牌:ID,访问和刷新令牌。 You can use the latter to request a new access token.
您可以使用后者请求新的访问令牌。
It is documented at : http://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html 它记录在: http : //docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.