使用AWS IOS SDK验证用户是否已通过身份验证

cdu*_*dub 3 objective-c amazon-web-services ios amazon-cognito aws-cognito

我创建了一个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
});
Run Code Online (Sandbox Code Playgroud)

它返回该用户的identityId和token.所有内容都使用IAM角色和AWS Cognito Identity进行设置,并且似乎在控制台中进行身份验证.

我有两个问题:

  1. 如何在应用程序中测试用户是否经过身份验证?我在应用设备中保存了identityId和token.
  2. 验证持续多长时间?我希望用户保持登录状态.这就是我使用的大多数应用程序工作并保持登录状态直到他们注销的方式.

谢谢.

iSk*_*ore 6

回答第一个问题:

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

您通过制作"自定义授权程序"来测试身份验证

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

或者你可以看看这个是同样的东西,只是在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 );
        } );
    }
};
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 转到Lambda并创建一个名为Auth_isValid或类似的函数
  2. 将您的PoolIdprincipalId放入环境变量中,以便以后更改
  3. 转到API网关并进行链接
  4. 在左侧的API选项下,点击 Authorizers
  5. 单击Create- >Custom Authorizer
  6. 填写你的Lambda区域,函数名称(应自动填充),授权者名称,身份令牌源(method.request.header.Authorization暂时保持简单,TTL可以是300.不要搞乱执行角色或令牌验证表达式.
  7. 保存/更新它并返回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;
    }
};
Run Code Online (Sandbox Code Playgroud)

基本上,我将一些我想要的项目包装在一个简单的加密密钥中,这样我就可以将所有信息传递给easy-peasy.(我将身份池作为哈希传递来使其变得很酷且简单,只要你永远不会将身份池ID发送到前端,我们就会很好!)

自定义授权器需要一个令牌,而不是你所说的"令牌"或其他东西的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 );
    } );
}
Run Code Online (Sandbox Code Playgroud)

更新:

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

{
    "type":"TOKEN",
    "authorizationToken":"<session_token>",
    "methodArn":"arn:aws:execute-api:<region>:<Account_ID>:<API_ID>/<Stage>/<Method>/<Resource_Path>"
}
Run Code Online (Sandbox Code Playgroud)

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

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "execute-api:Invoke",
            "Effect": "Deny",
            "Resource": [
                "arn:aws:execute-api:<region>:<Account_ID>:<API_ID>/<Stage>/*/*"
            ]
        }
    ]
}
Run Code Online (Sandbox Code Playgroud)

取决于我们的授权方式.


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

其次,我解密令牌,并确保try-catch尝试顺利.如果它不顺利,他们就是Unauthorized.如果确实如此,我们可以Continue.

您将在令牌中看到,我放了一个变量Expiration,这就是我如何检查密钥是否曾被接受并且正确并且现在只是过期了.为此,我说Proxy Authentication Required.这告诉了我的前端,再次登录并给我新的信誉.不要忘记,此功能的目的只是检查我们是否获得授权.不做像刷新令牌这样的花哨的东西.

接下来,我检查一切是否正常并调用denyAllMethods并将响应代码放入context响应中.API网关是非常挑剔,只希望通过周围简单地格式化IAM策略- 没有任何其他信息或格式,或者如果它没有指定的任何可能在那里这里这里

如果一切正常,我打电话getCredentialsForIdentity- 使用IdentityIdToken,确保令牌实际上也是有效的,然后我允许当时所需的功能.这些非常重要,只会将令牌验证为那些功能 - 换句话说.如果您在IAM IAM角色说,它可以访问所有内容,这也就不多说,你只能访问GET/userDELETE/user.所以不要让它欺骗你.毕竟这是一个自定义授权者.

接下来,我需要向您展示如何将所有这些放入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 ) );
        } );
    } );
};
Run Code Online (Sandbox Code Playgroud)

上面注意到:

duration

部分.

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

验证持续多长时间?我希望用户保持登录状态.这就是我使用的大多数应用程序工作并保持登录状态直到他们注销的方式.

您创建一个OpenIdToken使用任何你想找出他们和他们的电子邮件或TokenDuration处于.我建议这样做一两个星期但如果你想要一年或者其他东西,那31536000就是它.另一种方法是创建一个只为您提供授权凭据的函数,而不是denyAll407场景出现时调用授权程序,而是创建他们可以调用的唯一方法allowMethod( POST, /updateCreds );或类似的方法.这样你就可以每隔一段时间刷新一次.

伪的是:

去掉:

if( response.statusCode >= 400 )
else
Run Code Online (Sandbox Code Playgroud)

并做:

if( statusCode >= 400 )
    denyAll
else if( statusCode === 407 )
    allow refresh function
else
    allow everything else
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助!