使用 AWS IOS SDK 验证用户已通过身份验证 [英] Verifying user is authenticated using AWS IOS SDK

查看:47
本文介绍了使用 AWS IOS SDK 验证用户已通过身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我创建了一个执行以下操作的 lamdba 函数:

var 参数 ={IdentityPoolId: "us-east-1:the-full-identity-id",Logins: {}//在变量中包含提供者名称};param.Logins["com.test.website.login"] = userIdICreatedAndStoredInDynamoDB;cognitoidentity.getOpenIdTokenForDeveloperIdentity(param,功能(错误,数据){如果(错误)返回 fn(错误);//发生错误else fn(null, data.IdentityId, data.Token);//成功响应});

它返回该用户的身份标识和令牌.一切都使用 IAM 角色和 AWS Cognito Identity 进行设置,并且似乎正在控制台中进行身份验证.

我有两个问题:

  1. 如何在应用中测试用户已通过身份验证?我将身份标识和令牌保存在应用设备中.
  2. 身份验证持续多长时间?我希望用户保持登录状态.这就是我使用的大多数应用程序的工作方式,并在他们退出登录之前保持登录状态.

谢谢.

解决方案

回答第一个问题:

<块引用>

如何在应用中测试用户是否通过身份验证?我将 identityId 和令牌保存在应用设备中.

您通过创建自定义授权方"来测试身份验证

当你去创建一个新函数时,你可以在 Lambda 示例函数中找到的 AWS 示例函数(如果你过滤到 NodeJS 4.3 函数,它在后面)

或者你可以看看THIS 这是同样的事情,只是在 GitHub 上.

我在这里做了一个sorta修改版:

严格使用";常量代码 = {100:继续",101:交换协议",102:处理",200:OK",201:创建",202:接受",203:非权威信息",204:无内容",205:重置内容",206:;部分内容",207:多状态",208:已经报告",226:IM使用",300:多项选择",301:永久移动",302:找到",303:查看其他",304:未修改",305:使用代理",307:";临时重定向",308:永久重定向",400:错误请求",401:未经授权",402:需要付款",403:禁止",404:未找到",405:方法不允许",406:;不可接受",407:需要代理身份验证",408:请求超时",409:冲突",410:消失",411:需要长度",412:前提条件失败";, 413: Payload Too Large", 414: URI Too Long",415:不支持的媒体类型",416:无法满足的范围",417:期望失败",418:我是茶壶",421:错误的请求",422:无法处理"实体",423:锁定",424:失败的依赖关系",425:无序集合",426:需要升级",428:需要先决条件",429:请求太多",431:请求头字段太大",451:由于法律原因不可用",500:内部服务器错误",501:未实现",502:坏网关",503:服务不可用",504:网关超时",505:不支持 HTTP 版本",506:变体也协商",507:存储不足",508:检测到循环",509:超出带宽限制",510:未扩展",511:需要网络"认证;},响应=(状态代码,数据)=>( { statusCode, message: 代码[ statusCode ], data } ),AWS = 要求(aws-sdk"),加密 = 要求(加密"),COG = 新 AWS.CognitoIdentity(),令牌 = {算法:aes-256-ctr",加密:项目=>{item = JSON.stringify( item );让 cipher = crypto.createCipher( token.algorithm, process.env.PoolId ),crypted = cipher.update( item, 'utf8', 'base64' );加密 += cipher.final('base64');返回加密;},解密:项目=>{let decipher = crypto.createDecipher( token.algorithm, process.env.PoolId ),dec = decipher.update( item, 'base64', 'utf8' );dec += decipher.final( 'utf8' );返回十二月;}};功能 AuthPolicy(主体,awsAccountId,apiOptions){this.awsAccountId = awsAccountId;this.principalId = 委托人;this.version = '2012-10-17';this.pathRegex = new RegExp( '^[/.a-zA-Z0-9-*]+$' );this.allowMethods = [];this.denyMethods = [];if( !apiOptions || !apiOptions.restApiId ) this.restApiId = '*';否则 this.restApiId = apiOptions.restApiId;if( !apiOptions || !apiOptions.region ) this.region = '*';否则 this.region = apiOptions.region;if( !apiOptions || !apiOptions.stage ) this.stage = '*';否则 this.stage = apiOptions.stage;}AuthPolicy.HttpVerb = {获取:'获取',POST: 'POST',放放',补丁:'补丁',头:'头',删除: '删除',选项:'选项',全部: '*',};AuthPolicy.prototype = ( 函数 AuthPolicyClass() {函数添加方法(效果,动词,资源,条件){if( 动词 !== '*' && !Object.prototype.hasOwnProperty.call( AuthPolicy.HttpVerb, 动词 ) ) {throw new Error( `Invalid HTTP verb ${verb}. AuthPolicy.HttpVerb` 中允许的动词);}如果(!this.pathRegex.test(资源))throw new Error( `无效的资源路径:${resource}.路径应该匹配 ${this.pathRegex}`);让cleanedResource = 资源;if( resource.substring( 0, 1 ) === '/' )清洁资源 = 资源.子字符串(1,资源长度);const resourceArn = `arn:aws:execute-api:${this.region}:${this.awsAccountId}:${this.restApiId}/${this.stage}/${verb}/${cleanedResource}`;if( effect.toLowerCase() === '允许' )this.allowMethods.push({资源阿恩,状况,});否则 if( effect.toLowerCase() === '拒绝' )this.denyMethods.push({资源阿恩,状况,});}函数 getEmptyStatement( 效果 ) {const 语句 = {};statement.Action = 'execute-api:Invoke';statement.Effect = effect.substring( 0, 1 ).toUpperCase() + effect.substring( 1, effect.length ).toLowerCase();statement.Resource = [];退货声明;}函数getStatementsForEffect(效果,方法){const 语句 = [];如果(方法.长度> 0){const 语句 = getEmptyStatement( 效果 );for( 让 i = 0; i < methods.length; i++ ) {const curMethod = 方法 [ i ];if( curMethod.conditions === null || curMethod.conditions.length === 0 )statement.Resource.push( curMethod.resourceArn );别的 {const conditionalStatement = getEmptyStatement( effect );conditionalStatement.Resource.push( curMethod.resourceArn );conditionalStatement.Condition = curMethod.conditions;statements.push(conditionalStatement);}}if( statement.Resource !== null && statement.Resource.length > 0 )语句.推(语句);}返回语句;}返回 {构造函数:AuthPolicy,允许所有方法(){addMethod.call( this, 'allow', '*', '*', null );},拒绝所有方法(){addMethod.call( this, '拒绝', '*', '*', null );},允许方法(动词,资源){addMethod.call( this, 'allow', 动词, 资源, null );},拒绝方法(动词,资源){addMethod.call( this, 'deny', 动词, 资源, null );},allowMethodWithConditions(动词,资源,条件){addMethod.call( this, 'allow', 动词, 资源, 条件);},denyMethodWithConditions(动词,资源,条件){addMethod.call( this, 'deny', 动词, 资源, 条件);},建造() {if( ( !this.allowMethods || this.allowMethods.length === 0 ) &&( !this.denyMethods || this.denyMethods.length === 0 ) )throw new Error('没有为策略定义语句');常量策略 = {},文档 = {};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 ) );政策.policyDocument = doc;退货政策;},};} () );export.handler = ( 事件, 上下文, cb ) =>{常量principalId = process.env.principalId,tmp = event.methodArn.split( ':' ),apiGatewayArnTmp = tmp[5].split('/'),awsAccountId = tmp[4],apiOptions = {区域:tmp[3],restApiId:apiGatewayArnTmp[0],阶段:apiGatewayArnTmp[1]},策略 = 新 AuthPolicy( principalId, awsAccountId, apiOptions );让回应;if( !event.authorizationToken || typeof event.authorizationToken !== "string" )响应=响应(401);让 item = token.decrypt( event.authorizationToken );尝试 { item = resp( 100, JSON.parse( item ) );}catch( e ) { item = resp( 401 );}if( item.statusCode !== 100 )响应=响应(401);else if( item.data.Expiration <= new Date().getTime() )响应=响应(407);别的响应 = 响应(100);if( response.statusCode >= 400 ) {policy.denyAllMethods();const authResponse = policy.build();authResponse.context = 响应;cb(null, authResponse );} 别的 {COG.getCredentialsForIdentity({IdentityId: item.data.IdentityId,登录:{'cognito-identity.amazonaws.com':item.data.Token}}, ( e, d ) =>{如果( e ) {policy.denyAllMethods();响应=响应(401);} 别的 {policy.allowMethod(AuthPolicy.HttpVerb.GET, /user");policy.allowMethod( AuthPolicy.HttpVerb.DELETE, "/user");响应=响应(202);}const authResponse = policy.build();authResponse.context = 响应;cb(null, authResponse );});}};

上面是完整的例子...但是让我分解一下并解释为什么他们提供的没有那么有用.

以下是设置它的步骤,以便您了解为什么它必须是这样的.

  1. 转到 Lambda 并创建一个名为 Auth_isValid 或类似的函数
  2. 将您的 PoolIdprincipalId 放入环境变量中,以便以后轻松更改
  3. 前往 API Gateway 并让我们将其链接起来
  4. 在左侧的 API 选项下,点击 Authorizers
  5. 点击创建 ->自定义授权方
  6. 填写您的 Lambda 区域、函数名称(应自动填充)、授权方名称、身份令牌源(暂时使用 method.request.header.Authorization 保持简单,TTL 可以是300. 暂时不要搞乱执行角色或令牌验证表达式.
  7. 保存/更新它并返回到 Lambda - 我们稍后将与此授权方连接一个函数.

好吧,当你查看我的函数时,你会发现我在最顶部做了这个奇怪的加密/解密:

令牌 = {算法:aes-256-ctr",加密:项目=>{item = JSON.stringify( item );让 cipher = crypto.createCipher( token.algorithm, process.env.PoolId ),crypted = cipher.update( item, 'utf8', 'base64' );加密 += cipher.final('base64');返回加密;},解密:项目=>{let decipher = crypto.createDecipher( token.algorithm, process.env.PoolId ),dec = decipher.update( item, 'base64', 'utf8' );dec += decipher.final( 'utf8' );返回十二月;}};

基本上,我将一些我想要的项目包装在一个简单的加密密钥中,这样我就可以将所有信息传递给 easy-peasy.(我将身份池作为散列传入,以使其既酷又简单,只要您从不将身份池 ID 发送到前端,我们就很好!)

自定义授权方需要一个令牌,而不是您所说的令牌"的 JSON 块;或者什么(你可以做但看起来很蠢)

所以我们有一个统一的令牌被传入,我调用 decrypt 函数来解包(稍后我将展示加密示例.

现在有些人可能会说哦,好吧,这实际上不是加密,它很容易被弄清楚";- 我对此的回答是:好吧,无论如何,它本来是未加密的原始文本,为什么不让它变得容易."

好的,现在你看到了那个部分,向下到函数的底部.

让响应;if( !event.authorizationToken || typeof event.authorizationToken !== "string" )响应=响应(401);让 item = token.decrypt( event.authorizationToken );尝试 { item = resp( 100, JSON.parse( item ) );}catch( e ) { item = resp( 401 );}if( item.statusCode !== 100 )响应=响应(401);else if( item.data.Expiration <= new Date().getTime() )响应=响应(407);别的响应 = 响应(100);if( response.statusCode >= 400 ) {policy.denyAllMethods();const authResponse = policy.build();authResponse.context = 响应;cb(null, authResponse );} 别的 {COG.getCredentialsForIdentity({IdentityId: item.data.IdentityId,登录:{'cognito-identity.amazonaws.com':item.data.Token}}, ( e, d ) =>{如果( e ) {policy.denyAllMethods();响应=响应(401);} 别的 {policy.allowMethod(AuthPolicy.HttpVerb.GET, /user");policy.allowMethod( AuthPolicy.HttpVerb.DELETE, "/user");响应=响应(202);}const authResponse = policy.build();authResponse.context = 响应;cb(null, authResponse );});}

更新:

<块引用>

我们从 API Gateway 传入的数据是:

<代码>{类型":令牌",authorizationToken":","methodArn":"arn:aws:execute-api:::///"}

<块引用>

我们从 Lambda 发出的数据应该是这样的:

<代码>{版本":2012-10-17",声明":[{操作":execute-api:Invoke",效果":拒绝",资源":["arn:aws:execute-api:<region>:<Account_ID>:<API_ID>/<Stage>/*/*";]}]}

<块引用>

取决于我们的授权方式.

所以在我的第一次 if 检查中,我确保 authorizationToken 在那里并且它是一个 string,如果不是,我们说它是 Unauthorized(每个人都应该知道并使用他们的状态代码)

其次,我解密令牌并确保通过 try-catch 尝试顺利进行.如果不顺利,他们是未授权.如果是这样,我们可以继续.

您会在令牌中看到,我放置了一个变量 Expiration,这就是我检查密钥是否曾经被接受和正确以及现在是否已过期的方式.为此,我说需要代理身份验证.这告诉我的前端,再次调用登录并给我新的信用.不要忘记,这个函数的目的只能是检查我们是否被授权.不要做一些花哨的事情,比如刷新令牌.

接下来,我检查一切是否正常并调用 denyAllMethods 并将响应代码放在响应的 context 中.API 网关非常挑剔,只想要简单地传递 IAM 格式的策略 - 没有其他信息或格式,或者如果未指定则可能存在任何内容 这里这里

如果一切正常,我调用 getCredentialsForIdentity - 使用 IdentityIdToken,确保令牌实际上也有效,然后我允许当时需要的功能.这些非常重要,并且将仅针对这些功能验证令牌 - 换句话说.如果您在 IAM 中的 IAM 角色说它可以访问所有内容,这将说不,您只能访问 /user 上的 GETDELETE 上的<代码>/用户.所以不要让它愚弄你.毕竟这是一个自定义授权方.

接下来,我需要向您展示我是如何从登录部分放入所有这些内容的.我有相同的 token = { 部分,但在我的登录功能中我添加了一个 getToken 功能:

token.getToken = obj =>{返回新的承诺(( res,rej )=> {COG.getOpenIdTokenForDeveloperIdentity( {IdentityPoolId:process.env.PoolId,登录:{com.whatever.developerIdthing":obj.email},TokenDuration:持续时间}, ( e, r ) =>{r.Expiration = new Date().getTime() + ( duration * 1000 );如果( e ) rej( e );else res( token.encrypt( r ) );});});};

注意上面的内容:

持续时间

部分.

这是您的第二个问题的答案:

<块引用>

认证持续多长时间?我希望用户保持登录状态.这就是我使用的大多数应用程序的工作方式,并在他们退出登录之前保持登录状态.

您使用他们的电子邮件或任何您想识别他们的方式创建一个 OpenIdTokenTokenDuration 只需 .我建议做一两个星期,但如果你想要一年之类的,31536000 就是它.另一种方法是创建一个只为您提供授权凭据的函数,而不是在出现 407 场景时在授权器中调用 denyAll,而是 make the only 方法他们可以调用 allowMethod( POST,/updateCreds ); 或类似的东西.这样你就可以不时刷新他们的东西.

它的伪代码是:

删除:

if( response.statusCode >= 400 )别的

然后做:

if( statusCode >= 400 )全部拒绝否则如果(状态码 === 407 )允许刷新功能别的允许其他一切

希望这有帮助!

I created a lamdba function which does the following:

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. Everything is setup with IAM roles and AWS Cognito Identity and appears to be authenticating in the console.

I have two questions:

  1. How do I test in the app that the user is authenticated? I save the identityId and token in the app device.
  2. 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.

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.

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)

Or you can take a look at THIS which is the same thing, just on GitHub instead.

I made a sorta modified version here:

"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.

  1. Go to Lambda and make a function called Auth_isValid or something like that
  2. Put your PoolId and principalId into the Environment Variables so it's easy to change later
  3. Head over to API Gateway and lets link this up
  4. Under API Options on the left side, hit Authorizers
  5. Click Create -> Custom Authorizer
  6. Fill in your Lambda Region, function name (should auto-fill), Authorizer name, Identity Token Source (keep it simple with method.request.header.Authorization for now, and TTL can be 300. Lets not mess with Execution role or token validation expression yet.
  7. Save/Update it and head back to Lambda - we'll hook up a function with this authorizer later.

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!)

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)

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.

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:

{
    "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.

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)

Second, I decrypt the token and make sure that went well with a try-catch attempt. If it didn't go well, they're Unauthorized. if it did, we can 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. For this, I say 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. 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

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. 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. 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. I have the same token = { part but in my login function I added a getToken function:

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. I would recommend making this a week or two but if you wanted a year long or something, 31536000 would be it. 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 ); 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!

这篇关于使用 AWS IOS SDK 验证用户已通过身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆