简体   繁体   English

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

[英]Verifying user is authenticated using AWS IOS SDK

I created a lamdba function which does the following: 我创建了一个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
});

It returns the identityId and token for that user. 它返回该用户的identityId和token。 Everything is setup with IAM roles and AWS Cognito Identity and appears to be authenticating in the console. 所有内容都使用IAM角色和AWS Cognito Identity进行设置,并且似乎在控制台中进行身份验证。

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. 我在应用设备中保存了identityId和token。
  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. 我在应用设备中保存了identityId和token。

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) 当您创建一个新函数时,您可以在Lambda示例函数中找到AWS示例函数(如果您过滤到NodeJS 4.3函数,它将向后移动)

Or you can take a look at THIS which is the same thing, just on GitHub instead. 或者你可以看看这个是同样的东西,只是在GitHub上。

I made a sorta modified version here: 我在这里做了一个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 );
        } );
    }
};

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 转到Lambda并创建一个名为Auth_isValid的函数或类似的东西
  2. Put your PoolId and principalId into the Environment Variables so it's easy to change later 将您的PoolIdprincipalId放入环境变量中,以便以后更改
  3. Head over to API Gateway and lets link this up 转到API网关并进行链接
  4. Under API Options on the left side, hit Authorizers 在左侧的API选项下,点击Authorizers
  5. Click Create -> Custom Authorizer 单击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. 填写你的Lambda Region,函数名称(应自动填充),Authorizer名称,Identity Token Source(现在使用method.request.header.Authorization保持简单,TTL可以是300.不要乱用执行角色或令牌验证表达呢。
  7. Save/Update it and head back to Lambda - we'll hook up a function with this authorizer later. 保存/更新它并返回Lambda - 稍后我们将与此授权程序连接一个函数。

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!) (我将身份池作为哈希传递来使其变得很酷且简单,只要你永远不会将身份池ID发送到前端,我们就会很好!)

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) 自定义授权器需要一个令牌,而不是你所说的“令牌”或其他东西的JSON块(你可以做但看起来很愚蠢)

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. 所以我们有一个统一的令牌传入,我调用decrypt函数来解包(我将在一秒钟内显示加密示例)。

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: 我们从API Gateway传入的数据是:

{
    "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: 我们从Lambda传出的数据应该是这样的:

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

Second, I decrypt the token and make sure that went well with a try-catch attempt. 其次,我解密令牌,并确保try-catch尝试顺利。 If it didn't go well, they're Unauthorized . 如果进展不顺利,他们就是Unauthorized if it did, we can Continue . 如果确实如此,我们可以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. 你会在令牌中看到我输入一个变量Expiration ,这就是我如何检查密钥是否曾被接受并且正确并且现在只是过期了。 For this, I say Proxy Authentication Required . 为此,我说需要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. 接下来,我检查一切是否正常并调用denyAllMethods并将响应代码放在响应的context中。 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 API网关是非常挑剔,只希望通过周围简单地格式化IAM策略- 没有任何其他信息或格式,或者如果它没有指定的任何可能在那里这里这里

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. 如果一切正常,我调用getCredentialsForIdentity - 使用IdentityIdToken ,确保该令牌实际上也是有效的,然后我允许当时所需的功能。 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 . 如果您在IAM IAM角色说,它可以访问所有内容,这也就不多说,你只能访问GET/userDELETE/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. 接下来,我需要向您展示如何将所有这些放入Login部分。 I have the same token = { part but in my login function I added a getToken function: 我有相同的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 ) );
        } );
    } );
};

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 . 您可以使用他们的电子邮件或任何您想要识别它们的内容创建OpenIdToken并且TokenDuration几秒钟内完成 I would recommend making this a week or two but if you wanted a year long or something, 31536000 would be it. 我建议这一周或两周,但如果你想要一年或者其他东西, 31536000将是它。 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 ); 另一种方法是创建一个只提供授权凭据的函数,而不是在407场景出现时在授权器中调用denyAll ,而是创建唯一可以调用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! 希望这可以帮助!

To test if they're logged in you need to set up a service that'll check the token against Cognito. 要测试他们是否已登录,您需要设置一个服务,以便根据Cognito检查令牌。 Quick and dirty way is to set up a basic lambda, expose it through API Gateway with an authorizer pointed at your User Identity Pool. 快速而肮脏的方法是设置一个基本的lambda,通过API Gateway使用指向您的用户身份池的授权程序公开它。 All the lambda needs to do is return HTTP 200, since what you're really checking is the authorizer. 所有lambda需要做的是返回HTTP 200,因为你真正检查的是授权者。 Then have your app get/post/etc to that API URL w/ a header of "Authorization":$ACCESS_TOKEN. 然后让你的应用程序获取/发布/等到具有“授权”标题的API API:$ ACCESS_TOKEN。 either it'll kick back a 200 on success or it'll return an Unauthorized message. 或者它会在成功时放回200,否则它将返回未经授权的消息。

Your Cognito token is only good for an hour, but you can refresh the token to keep a person logged in. When your user authenticated they got three tokens: ID, Access, and Refresh token. 您的Cognito令牌仅适用于一小时,但您可以刷新令牌以保持用户登录。当您的用户通过身份验证时,他们会获得三个令牌:ID,访问和刷新令牌。 You can use the latter to request a new access token. 您可以使用后者请求新的访问令牌。

It is documented at : http://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html 它记录在: http//docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 从未认证切换到经过开发人员认证的Cognito用户-AWS iOS SDK - Switch from unauth to developer authenticated cognito user - AWS iOS SDK (iOS) AWS S3 上传失败且没有错误(使用联合身份验证的用户 - Apple SSO) - (iOS) AWS S3 Upload Fails with No Error (User Authenticated Using Federated Identities - Apple SSO) 使用AWS开发工具包iOS登录AWS控制台 - Signing In to AWS console using AWS SDK iOS 获取用户属性AWS Amplify iOS SDK - Get user attributes AWS Amplify iOS SDK 使用iOS SDK Cognito Lambda和DynamoDB为AWS iOS创建用户/开发人员定义的登录 - Creating a user/developer defined login for AWS iOS using iOS SDK Cognito Lambda and DynamoDB 是否需要在PayPal的iOS SDK中验证付款? - Verifying Payment in PayPal's iOS SDK necessary? 使用AWS开发工具包iOS版本2.6.1登录后无法访问用户详细信息userName,imageURL - Unable to access user details userName , imageURL after login using AWS SDK iOS version 2.6.1 使用自定义cname上传aws ios sdk - Using custom cname for uploads with aws ios sdk 在iOS上解析SDK:“ NSInternalInconsistencyException”,原因:“除非通过登录或注册进行了身份验证,否则无法保存用户” - Parse SDK on iOS: 'NSInternalInconsistencyException', reason: 'User cannot be saved unless they have been authenticated via logIn or signUp' 使用 RSA 在 iOS 上签名和验证 - Signing and Verifying on iOS using RSA
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM