验证用户已使用 AWS IOS SDK 进行身份验证
Verifying user is authenticated using AWS IOS SDK
我创建了一个执行以下操作的 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
});
它 returns 该用户的 identityId 和令牌。一切都使用 IAM 角色和 AWS Cognito Identity 进行设置,并且似乎正在控制台中进行身份验证。
我有两个问题:
- 如何在应用程序中测试用户是否已通过身份验证?我将 identityId 和 token 保存在应用设备中。
- 身份验证持续多长时间?我希望用户保持登录状态。这就是我使用的大多数应用程序的工作方式,并在他们点击注销之前一直保持登录状态。
谢谢。
要测试他们是否已登录,您需要设置一项服务来根据 Cognito 检查令牌。快速而肮脏的方法是设置一个基本的 lambda,通过 API 网关公开它,授权者指向您的用户身份池。 lambda 需要做的就是 return HTTP 200,因为您真正检查的是授权方。然后让你的应用 get/post/etc 到 API URL w/ header of "Authorization":$ACCESS_TOKEN。要么它会在成功时返回 200,要么它会 return 一条未经授权的消息。
您的 Cognito 令牌只能使用一个小时,但您可以刷新令牌以保持用户登录状态。当您的用户通过身份验证时,他们会获得三个令牌:ID、访问和刷新令牌。您可以使用后者来请求新的访问令牌。
回答第一个问题:
How do I test in the app that the user is authenticated? I save the identityId
and token in the app device.
您通过制作“自定义授权器”来测试身份验证
创建新函数时可以在Lambda Example Functions中找到的AWS示例函数
(如果过滤到 NodeJS 4.3 函数,它在后面)
或者你可以看看 THIS 是一样的,只是在 GitHub 上。
我在这里做了一个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 );
} );
}
};
以上是完整示例...但让我将其分解并解释为什么他们提供的示例没有那么有用。
以下是设置的步骤,因此您可以了解为什么它必须是这样的。
- 转到 Lambda 并创建一个名为
Auth_isValid
或类似名称的函数
- 将你的
PoolId
和 principalId
放入环境变量中,以便以后轻松更改
- 前往 API 网关并让 link 启动
- 在左侧的 API 选项下,点击
Authorizers
- 点击
Create
-> Custom Authorizer
- 填写您的 Lambda 区域、函数名称(应自动填写)、授权方名称、身份令牌源(暂时使用
method.request.header.Authorization
保持简单,TTL 可以是 300。不要搞乱执行尚未角色或令牌验证表达式。
- Save/Update 然后返回 Lambda - 我们稍后将与此授权方连接一个函数。
好的,所以当你查看我的函数时,你会发现我在最顶部做了这个奇怪的 encrypt/decrypt 事情:
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;
}
};
基本上,我将一些我想要的项目包装在一个简单的加密密钥中,这样我就可以轻松地传递我的所有信息。
(我将身份池作为散列传入,以使其既酷又简单,只要您从不将身份池 ID 发送到前端,我们就很好!)
Custom Authorizer 需要一个令牌,而不是 JSON 你所说的“令牌”块或其他东西(你可以这样做,但它看起来很笨)
所以我们有一个传入的统一令牌,我为此调用 decrypt
函数来解包(稍后我将展示加密示例。
现在有些人可能会说“哦,好吧,这实际上不是加密,它很容易被弄清楚”——我对此的回答是:“好吧,无论如何,它本来是未加密的,原始文本,为什么不让它变得简单。 “
好的,既然你看到了那部分,请继续往下看函数的底部。
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 );
} );
}
更新:
Our incoming data from API Gateway is:
{
"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:
{
"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.
所以在我的第一个 if
检查中,我确保 authorizationToken
在那里并且它是 string
,如果不是,我们说它是 Unauthorized
(每个人都应该知道并使用他们的状态代码)
其次,我解密了令牌并确保 try-catch
尝试顺利进行。如果进展不顺利,他们就是 Unauthorized
。如果是这样,我们可以 Continue
.
你会在令牌中看到,我放了一个变量 Expiration
,这就是我检查密钥是否曾经被接受和正确的方式,现在只是过期了。为此,我说 Proxy Authentication Required
。这告诉我的前端,再次调用登录并给我新的信用。别忘了,此功能的目的只能是检查我们是否获得授权。不要做刷新令牌等花哨的事情。
接下来,我检查是否一切正常并调用 denyAllMethods
并将响应代码放入响应的 context
中。 API 网关非常挑剔,只需要传递简单的 IAM 格式的策略 - 没有其他信息或格式,或者如果未指定则可能存在的任何内容 HERE or HERE
如果一切正常,我调用 getCredentialsForIdentity
- 使用 IdentityId
和 Token
,确保令牌实际上也是有效的,然后我允许函数当时需要。这些非常重要, 将仅针对那些函数验证令牌 - 换句话说。如果您在 IAM 中的 IAM 角色说它可以访问所有内容,这会说不,您只能访问 /user
上的 GET
和 /user
上的 DELETE
。所以不要让它愚弄你。毕竟这是 自定义授权人。
接下来,我需要向您展示我是如何从登录部分输入所有这些内容的。我有相同的 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 ) );
} );
} );
};
注意上面的:
duration
部分
这是您第二个问题的答案:
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.
您使用他们的电子邮件或您想要识别他们的任何内容创建一个 OpenIdToken
,并且 TokenDuration
在 秒 内完成。我建议做一两周,但如果你想要一年或更长时间,31536000
就可以了。另一种方法是制作一个只为您提供授权凭据的函数,而不是在出现 407
场景时在授权方中调用 denyAll
,制作他们可以调用的唯一方法 allowMethod( POST, /updateCreds );
或类似的东西。这样你就可以时不时刷新一下他们的东西。
伪造的是:
删除:
if( response.statusCode >= 400 )
else
然后做:
if( statusCode >= 400 )
denyAll
else if( statusCode === 407 )
allow refresh function
else
allow everything else
希望对您有所帮助!
我创建了一个执行以下操作的 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
});
它 returns 该用户的 identityId 和令牌。一切都使用 IAM 角色和 AWS Cognito Identity 进行设置,并且似乎正在控制台中进行身份验证。
我有两个问题:
- 如何在应用程序中测试用户是否已通过身份验证?我将 identityId 和 token 保存在应用设备中。
- 身份验证持续多长时间?我希望用户保持登录状态。这就是我使用的大多数应用程序的工作方式,并在他们点击注销之前一直保持登录状态。
谢谢。
要测试他们是否已登录,您需要设置一项服务来根据 Cognito 检查令牌。快速而肮脏的方法是设置一个基本的 lambda,通过 API 网关公开它,授权者指向您的用户身份池。 lambda 需要做的就是 return HTTP 200,因为您真正检查的是授权方。然后让你的应用 get/post/etc 到 API URL w/ header of "Authorization":$ACCESS_TOKEN。要么它会在成功时返回 200,要么它会 return 一条未经授权的消息。
您的 Cognito 令牌只能使用一个小时,但您可以刷新令牌以保持用户登录状态。当您的用户通过身份验证时,他们会获得三个令牌:ID、访问和刷新令牌。您可以使用后者来请求新的访问令牌。
回答第一个问题:
How do I test in the app that the user is authenticated? I save the
identityId
and token in the app device.
您通过制作“自定义授权器”来测试身份验证
创建新函数时可以在Lambda Example Functions中找到的AWS示例函数 (如果过滤到 NodeJS 4.3 函数,它在后面)
或者你可以看看 THIS 是一样的,只是在 GitHub 上。
我在这里做了一个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 );
} );
}
};
以上是完整示例...但让我将其分解并解释为什么他们提供的示例没有那么有用。
以下是设置的步骤,因此您可以了解为什么它必须是这样的。
- 转到 Lambda 并创建一个名为
Auth_isValid
或类似名称的函数 - 将你的
PoolId
和principalId
放入环境变量中,以便以后轻松更改 - 前往 API 网关并让 link 启动
- 在左侧的 API 选项下,点击
Authorizers
- 点击
Create
->Custom Authorizer
- 填写您的 Lambda 区域、函数名称(应自动填写)、授权方名称、身份令牌源(暂时使用
method.request.header.Authorization
保持简单,TTL 可以是 300。不要搞乱执行尚未角色或令牌验证表达式。 - Save/Update 然后返回 Lambda - 我们稍后将与此授权方连接一个函数。
好的,所以当你查看我的函数时,你会发现我在最顶部做了这个奇怪的 encrypt/decrypt 事情:
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;
}
};
基本上,我将一些我想要的项目包装在一个简单的加密密钥中,这样我就可以轻松地传递我的所有信息。 (我将身份池作为散列传入,以使其既酷又简单,只要您从不将身份池 ID 发送到前端,我们就很好!)
Custom Authorizer 需要一个令牌,而不是 JSON 你所说的“令牌”块或其他东西(你可以这样做,但它看起来很笨)
所以我们有一个传入的统一令牌,我为此调用 decrypt
函数来解包(稍后我将展示加密示例。
现在有些人可能会说“哦,好吧,这实际上不是加密,它很容易被弄清楚”——我对此的回答是:“好吧,无论如何,它本来是未加密的,原始文本,为什么不让它变得简单。 “
好的,既然你看到了那部分,请继续往下看函数的底部。
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 );
} );
}
更新:
Our incoming data from API Gateway is:
{
"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:
{
"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.
所以在我的第一个 if
检查中,我确保 authorizationToken
在那里并且它是 string
,如果不是,我们说它是 Unauthorized
(每个人都应该知道并使用他们的状态代码)
其次,我解密了令牌并确保 try-catch
尝试顺利进行。如果进展不顺利,他们就是 Unauthorized
。如果是这样,我们可以 Continue
.
你会在令牌中看到,我放了一个变量 Expiration
,这就是我检查密钥是否曾经被接受和正确的方式,现在只是过期了。为此,我说 Proxy Authentication Required
。这告诉我的前端,再次调用登录并给我新的信用。别忘了,此功能的目的只能是检查我们是否获得授权。不要做刷新令牌等花哨的事情。
接下来,我检查是否一切正常并调用 denyAllMethods
并将响应代码放入响应的 context
中。 API 网关非常挑剔,只需要传递简单的 IAM 格式的策略 - 没有其他信息或格式,或者如果未指定则可能存在的任何内容 HERE or HERE
如果一切正常,我调用 getCredentialsForIdentity
- 使用 IdentityId
和 Token
,确保令牌实际上也是有效的,然后我允许函数当时需要。这些非常重要, 将仅针对那些函数验证令牌 - 换句话说。如果您在 IAM 中的 IAM 角色说它可以访问所有内容,这会说不,您只能访问 /user
上的 GET
和 /user
上的 DELETE
。所以不要让它愚弄你。毕竟这是 自定义授权人。
接下来,我需要向您展示我是如何从登录部分输入所有这些内容的。我有相同的 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 ) );
} );
} );
};
注意上面的:
duration
部分
这是您第二个问题的答案:
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.
您使用他们的电子邮件或您想要识别他们的任何内容创建一个 OpenIdToken
,并且 TokenDuration
在 秒 内完成。我建议做一两周,但如果你想要一年或更长时间,31536000
就可以了。另一种方法是制作一个只为您提供授权凭据的函数,而不是在出现 407
场景时在授权方中调用 denyAll
,制作他们可以调用的唯一方法 allowMethod( POST, /updateCreds );
或类似的东西。这样你就可以时不时刷新一下他们的东西。
伪造的是:
删除:
if( response.statusCode >= 400 )
else
然后做:
if( statusCode >= 400 )
denyAll
else if( statusCode === 407 )
allow refresh function
else
allow everything else
希望对您有所帮助!