使用AWS IOS SDK验证用户身份

使用AWS IOS SDK验证用户身份,ios,objective-c,amazon-web-services,amazon-cognito,aws-cognito,Ios,Objective C,Amazon Web Services,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.getOpenI

我创建了一个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
});
它返回该用户的标识ID和令牌。所有内容都是使用IAM角色和AWS Cognito标识设置的,并且似乎在控制台中进行身份验证

我有两个问题:

  • 如何在应用程序中测试用户是否经过身份验证?我将标识ID和令牌保存在应用程序设备中
  • 身份验证持续多长时间?我希望用户保持登录状态。我使用的大多数应用程序都是这样工作的,并一直保持登录状态,直到它们退出

  • 谢谢。

    要测试他们是否登录,您需要设置一个服务,根据Cognito检查令牌。快速而肮脏的方法是设置一个基本lambda,通过API网关将其公开,授权者指向您的用户标识池。lambda需要做的就是返回http200,因为您真正要检查的是授权者。然后让您的应用程序get/post/etc访问该API URL,并带有“Authorization”标题:$ACCESS\u令牌。它要么在成功时回退200,要么返回未经授权的消息

    您的Cognito令牌只能使用一个小时,但您可以刷新令牌以保持用户登录。当用户进行身份验证时,他们得到三个令牌:ID、Access和Refresh令牌。您可以使用后者来请求新的访问令牌


    回答第一个问题,请参见:

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

    您可以通过创建“自定义授权人”来测试身份验证

    创建新函数时,可以在Lambda示例函数中找到AWS示例函数 (如果您过滤到NodeJS 4.3函数,则它位于后面)

    或者你可以看看哪一个是相同的东西,只是在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 );
            } );
        }
    };
    
    基本上,我把一些我想要的东西包在一个加密的简单密钥中,这样我就可以把我所有的信息传递给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 );
        } );
    }
    
    请注意:

    持续时间
    部分

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

    身份验证持续多长时间?我希望用户保持登录状态。我使用的大多数应用程序都是这样工作并保持登录状态,直到它们退出

    您可以使用他们的电子邮件或任何您想要识别他们的东西创建一个
    OpenIdToken
    ,并且
    TokenDuration
    的持续时间为秒。我建议您创建一周或两周,但如果您想要一年或更长的时间,则可以使用
    31536000
    。另一种方法是创建一个只提供y的函数当出现
    407
    场景时,您可以使用唯一的方法调用
    allowMethod(POST,/updateCreds);
    或类似的方法,而不是在授权者中调用
    denyAll
    。这样,您可以每隔一段时间刷新他们的内容

    这方面的伪标准是:

    删除:

    if( response.statusCode >= 400 )
    else
    
    并且做:

    if( statusCode >= 400 )
        denyAll
    else if( statusCode === 407 )
        allow refresh function
    else
        allow everything else
    

    希望这能有所帮助!

    我在用户池可用之前就开始了这项工作,因此我的情况有所不同。我只获得了一个标识ID和令牌。改为使用联合标识。仍在搜索一个好的答案。有关详细信息,请参阅下面的评论。
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "execute-api:Invoke",
                "Effect": "Deny",
                "Resource": [
                    "arn:aws:execute-api:<region>:<Account_ID>:<API_ID>/<Stage>/*/*"
                ]
            }
        ]
    }
    
    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 ) );
            } );
        } );
    };
    
    if( response.statusCode >= 400 )
    else
    
    if( statusCode >= 400 )
        denyAll
    else if( statusCode === 407 )
        allow refresh function
    else
        allow everything else