要回答第一個問題:
如何在用戶進行身份驗證的應用程序測試
它是在記錄?我將identityId
和令牌保存在應用設備中。
您通過進行「自定義授權者」測試認證
的AWS實例功能,您可以在lambda功能。例如發現當你去做出新的功能 (如果您篩選的NodeJS 4.3功能,它的背面)
或者你可以看看THIS這是同樣的事情,只是在GitHub上。
我在這裏做了一個八九不離十修改後的版本:
"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網關更改後
- 頭,讓鏈接這件事
- 在左側API選項,打
Authorizers
- 點擊
Create
- >Custom Authorizer
- 填寫您的Lambda區域,函數名稱(應自動填充),授權人名稱,身份令牌源(現在簡單地使用
method.request.header.Authorization
,並且TTL可以是300)。讓我們不要混淆執行角色或令牌驗證表達式。
- 保存/更新它並返回到Lambda - 稍後我們將使用此授權人與功能掛鉤。
好了,所以當你在看我的功能,你會看到,我做這種怪異的加密/在解密事情的頂部:
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;
}
};
基本上,我換一些項目我想裏面的加密密鑰很簡單,所以我可以將所有信息傳遞給易於使用的人。 (我將Identity Pool作爲散列傳遞給它,使其變得既酷又簡單,只要您從不將身份池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);
});
}
更新:
我們從API網關進入的數據是:
{
"type":"TOKEN",
"authorizationToken":"<session_token>",
"methodArn":"arn:aws:execute-api:<region>:<Account_ID>:<API_ID>/<Stage>/<Method>/<Resource_Path>"
}
我們從LAMBDA傳出的數據應該是這樣的:
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Deny",
"Resource": [
"arn:aws:execute-api:<region>:<Account_ID>:<API_ID>/<Stage>/*/*"
]
}
]
}
根據我們的授權情況而定。
所以在我的第一if
檢查,我確保authorizationToken
是存在的,它是一個string
,如果不是的話,我們說這是Unauthorized
(大家應該知道並使用他們的狀態碼)
其次,我解密令牌並確保嘗試執行try-catch
。如果它不好,他們是Unauthorized
。如果是這樣,我們可以Continue
。
你會看到在令牌中,我把一個變量Expiration
,這是我如何檢查密鑰是否被接受和正確,現在只是過期。爲此,我說Proxy Authentication Required
。它告訴我的前端,再次呼叫登錄並給我新的信譽。不要忘記,這個功能的目的只是爲了檢查我們是否被授權。不要做像刷新標記這樣的花式東西。
接下來,我檢查是否一切正常,並致電denyAllMethods
並將響應代碼放在響應的context
中。 API網關是非常挑剔,只想要簡單的IAM格式化政策傳來傳去 - 沒有其它信息或格式或任何可能在那裏,如果不是指定它HERE或HERE
如果一切正常,我叫getCredentialsForIdentity
- 使用IdentityId
和Token
,確保令牌實際上也是有效的,然後我允許當時需要的功能。這些是非常重要的,並且將驗證令牌僅僅是那些功能 - 換句話說。如果您在IAM中的IAM角色表示可以訪問所有內容,則將顯示爲否,您只能在/user
和上訪問GET
和DELETE
。所以不要讓它愚弄你。畢竟,這是一個定製授權人。
接下來,我需要向您展示如何將所有這些內容從Login部分放入。我有同樣的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
部分。
這是回答你的第二個問題:
多久認證持續多久?我希望用戶保持登錄狀態。這是我使用的大多數應用程序的工作方式,並保持登錄狀態,直到他們登出。
創建使用任何你想找出他們和TokenDuration
是秒他們的電子郵件或OpenIdToken
。我會建議做這一兩個星期,但如果你想要一年或什麼東西,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
希望這有助於!
仍在尋找一個好的答案。 – cdub
請參閱下面的評論以獲取更多信息。 – cdub