Чтобы ответить на первый вопрос:
Как проверить в приложении, которое аутентифицирован? Я сохраняю identityId
и токен в приложении.
Вы проверить подлинности, сделав «Custom Authorizer»
Пример функции 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
- Заполните свой лямбда-регион, имя функции (должно быть автозаполнено), имя авторизованного пользователя, источник токена идентичности (пока оно будет оставаться простым с
method.request.header.Authorization
, а TTL может быть 300. Позволяет не испортить роль Execution или выражение проверки токена.
- Сохраните/обновите его и вернитесь в Лямбду - мы подключим функцию с этим авторизатором позже.
ИТАК, когда вы посмотрите на мою функцию, вы увидите, что я делаю это странное шифровать/дешифровать вещь на самом верху:
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;
}
};
В принципе, я обернуть некоторые элементы, которые я хочу внутри зашифрованный ключ, так что я могу передать всю свою информацию вокруг easy-peasy. (я передаю в Identity Pool в качестве хэша, чтобы сделать его крутым и простым, и до тех пор, пока вы никогда не отправляете идентификатор Pool Identity на передний план, мы хороши!)
Пользовательский авторизатор требует один единственный токен , а не блок 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);
});
}
Update:
Наши данные, поступающие от 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 говорит, что она может получить доступ ко всему, это скажет «нет», вы можете получить доступ только к GET
по телефонам /user
и DELETE
по адресу /user
. Так что не позволяйте этому обманывать вас. Это пользовательский авторизатор в конце концов.
Далее, я должен показать вам, как я все это ввел из части входа. У меня такая же 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
часть.
Это ответ на ваш второй вопрос:
Сколько времени аутентификации в последний раз? Я хочу, чтобы пользователь оставался вошедшим в систему. Это то, как большинство приложений, которые я использую, работают и остаются в системе, пока они не удалят выход из системы.
Вы создать OpenIdToken
используя свою электронную почту или что вы хотите, чтобы идентифицировать их и TokenDuration
находится в секунд. Я бы рекомендовал сделать это неделю или две, но если бы вы хотели год или что-то, 31536000
было бы это.Другой способ сделать это - сделать функцию, которая дает только авторизованные учетные данные, и вместо вызова denyAll
в авторизаторе при возникновении сценария 407
, сделайте единственный метод, который они могут назвать 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