Cognito UserPool + IdentityPools + AssumeRoleWithWebIdentity

0

I can not revoke the temporary IAM access Token when sign-in with a different user using the same HTML Page.

Configuration Cognito User Pool:

  1. User Attributes: Mail, Phone Number
  2. MFA (enabled by Google Auth)
  3. Device Tracking (enabled)
  4. Group Name: Admin
  5. Group Name Role: S3-Access-Role
  6. Trust Entities
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "cognito-identity.amazonaws.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "cognito-identity.amazonaws.com:aud": "us-east-1:bla-bla-bla"
                },
                "ForAnyValue:StringLike": {
                    "cognito-identity.amazonaws.com:amr": "authenticated"
                }
            }
        }
    ]
}

Permission Policy Attached to S3-Access-Role : Amazon S3 Full Access

APP Integration:

Secret Code: blablabla

Authentication flows:

ALLOW_REFRESH_TOKEN_AUTH

ALLOW_USER_SRP_AUTH

Cognito Identity Pool

Identity Provider: us-east-1_{UserPoolID}

Identity Provider Role Settings: Role with preferred_role claim in tokens

(I am not very clear about this setting)

Basic Authentication: Activate basic flow (Checked)

User access authentication role: Lambda-Access-Role (not access to S3)

Now, I have two users: Elena and Laura.

Elena is in Group Admin,

Laura is NOT in Group Admin

Laura Sign-In and obtain: accessToken, idToken and refreshToken and call a SDK javascript v3 function with the idToken:


import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; 
import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers";

export const GetObjectBucket = async (idToken) =>{

    let credentials = fromCognitoIdentityPool({
        clientConfig: { region: 'us-east-1' }, // Configure the underlying CognitoIdentityClient.
        identityPoolId: 'us-east-1:blablabla',
        logins: {
            'cognito-idp.us-east-1.amazonaws.com/us-east-1_{UserPoolId}': idToken,
        }
    });
    const s3Client = new S3Client({
        region: 'us-east-1',
        credentials: credentials
    });
    const input = {Bucket:'bbb', Key:'aaa.txt'};
    const command = new GetObjectCommand(input);
    return(await s3Client.send(command));

Returns: Exception Access Denied, Good!. Because Laura has no permission to access S3

Now, Elena Sign-In (I am using the same HTLM static page for Sign-In)

Returns the bucket object, Good!, Because Elenas is in the Group of User Pool that has permission to access S3

But Now, Laura signs in again, and instead of returning: Access Denied, get the object without any problem. Bad news!

Trying on a different HTML Page (open in Incognito mode) it works as expected.

I guess the problem is that the temporary token with IAM credentials is stored locally in a cookie and it has an expiration time. I don't know how to force it to revoke.

I tried: Close the session for both users (Laura and Elena) in Cognito AWS Console (it does not work)

Add a Policy AWSRevokeOlderSessions in S3-Access-Role (it does not work)

So, How can I revoke the temporary token with IAM credentials in a programmatic way?

On the other hand, when I try to revoke the idToken using:

const revokeToken = async ({token, clientId, clientSecret})=>{
    const client = new CognitoIdentityProviderClient({region:'us-east-1'});
    const input = { // RevokeTokenRequest
      Token: token, // required
      ClientId: clientId, // required
    };
    if(clientSecret)
      input['ClientSecret'] = clientSecret;
    const command = new RevokeTokenCommand(input);
    const r = await client.send(command);
    return(r);
};

I get UnsupportedTokenTypeException: Unsupported token type

The same if I pass the accessToken instead of idToken.

I do not know why.

Thank you in advance

4 Answers
2

The temporary credentials issued by STS are only a cryptographically signed set of tokens, with no mechanism to revoke them explicitly. The indirect but explicit mechanism available would be to modify the access policy of the IAM role to apply an explicit Deny to actions taken with the credentials. The credentials would still be technically valid (for example, sts:GetCallerIdentity would still accept them and return the identity of the caller), but the permissions policy would block all actions requiring authorisation from being taken with them.

Policy changes propagate very rapidly, even if not quite instantaneously. However, applying those kinds of explicit denies through policy changes wouldn't scale beyond the modest policy size limitations, and removing the denies after the credentials have expired would probably be tedious at best.

Lots of specifics on using policies this way are documented here: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_disable-perms.html

If you're using the temporary credentials specifically to allow users access to S3, would it be an option to use presigned URLs in S3 (https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) or CloudFront (https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html)? The signed URLs can be made specific to a single object URL, and their validity times can be adjusted appropriately for each situation.

Or alternatively, could you make the validity time of the temporary credentials from STS short enough that they wouldn't meaningfully need to be revoked when the user logs off?

EXPERT
answered 10 months ago
profile picture
EXPERT
reviewed 10 months ago
profile pictureAWS
EXPERT
reviewed 10 months ago
1

Yes, underneath, the Cognito API GetCredentialsForIdentity (https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetCredentialsForIdentity.html) gets called and temporary credentials from STS of the form shown in the documentation link are returned. What you do with them after placing them in the credentials variable, such as storing them in a cookie, you'd have to check from your code.

The Deny policy statement is of the correct form, although I doubt it'll work for your use case, since it will invalidate all tokens issued before the specified time for all users using the role. I think you posted your reply at 20:00 UTC and the timestamp compared in the policy is 16:40 UTC. Did you test it for a session from several hours before posting the reply?

On a general note, unless this is an internal service for trusted users, it sounds quite complex to allow end users to access all those services directly. Of course, you could use policy statements to control what they can do in DynamoDB, for example, but that's limited to what can be expressed in the policy language. Simple rate-limiting wouldn't even be possible, so anyone logging on could cause arbitrarily high costs for you simply by calling the services with their temporary credentials. Is accessing the services directly from client-side code an intentional choice? I would think many of these access control issues would be simpler if your clients accessed your custom APIs via API Gateway, and you would handle authentication, authorisation, and request validation there. API Gateway also supports WAFv2 and being placed behind a CloudFront distribution for rate-limiting, geo-restrictions, and other general protections.

EXPERT
answered 10 months ago
0

Thank you Leo,

But I need the IAM STS temporary credentials, not only to access S3 but also DynamoDB, Lambda, and other AWS services, and the roles are apply to different kinds of user groups in Cognito UserPool.

My problem is easy:

When a user Sign-in (using an HTML static page), it obtains from Cognito User Pool: idToken, accessToken and refreshToken, then I pass the idToken to the function fromCognitoIdentityPool and obtain the IAM temporary access token:

import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers";
let credentials = fromCognitoIdentityPool({
        clientConfig: { region: 'us-east-1' },
        identityPoolId: 'us-east-1:blablabla',
        logins: {
            'cognito-idp.us-east-1.amazonaws.com/us-east-1_{UserPoolId}': idToken,
        }
    });

I suppose (not sure), that when this is done, the IAM temporary credential (token) is sent and stored in the cache browser. So, when a new user Sign-In that has NO permission to access a resource, the browser sends the cache credential and it is accepted without looking the permissions for that user (new Sign-in, different idToken).

In this case, the solution is out of the AWS scope, I need to detect a new user Sign-in and Clear the Browser Cache. But, I do not know how :(

Anyway, there is something that catches my attention. If I add a policy: AWSRevokeOlderSessions to the role, the credential still working.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Action": [
                "*"
            ],
            "Resource": [
                "*"
            ],
            "Condition": {
                "DateLessThan": {
                    "aws:TokenIssueTime": "2024-07-13T16:40:16.546Z"
                }
            }
        }
    ]
}

Best Regards, marcelo

profile picture
answered 10 months ago
0

Thank you Leo, thank you for your time

Some comments before closing the case.

"The Deny policy statement is of the correct form, although I doubt it'll work for your use case, since it will invalidate all tokens issued before the specified time for all users using the role. I think you posted your reply at 20:00 UTC and the timestamp compared in the policy is 16:40 UTC. Did you test it for a session from several hours before posting the reply?"

Yes, you are right, the revoke all sessions works:

Role: X only one policy: AmazonS3FullAccess

Role X Maximum session duration: 1 hour

Login Laura (in group with Role X) at: 17:34 UTC

GetBucketObject() at: 17:35 UTC

Response httpStatusCode: 200 OK

Add Policy to Role X: AWSRevokeOlderSessions at: *17:45 UTC

"Condition": 
{
	"DateLessThan": {
		"aws:TokenIssueTime": "2024-07-14T17:45:55.373Z"
	}
}

GetBucketObject() at: 17:50 UTC

NotAuthorizedException: Invalid login token. Token expired: 1720979028 >= 1720979023

Good, because I revoke all sessions for this role.

Login Laura (again) at: 18:00 UTC

GetBucketObject() at: 18:05 UTC

Response httpStatusCode: 200 OK

GetBucketObject() at: 19:10 UTC

NotAuthorizedException: Invalid login token. Token expired: 1720983964 >= 1720980597

Good, because the role has a 1-hour session duration.

Anyway, the temporary credential is still active (I guess it has a default of12 hours).

"On a general note, unless this is an internal service for trusted users, it sounds quite complex to allow end users to access all those services directly."

Yes, of course, it is only for testing purposes.

"so anyone logging on could cause arbitrarily high costs for you simply by calling the services with their temporary credentials. Is accessing the services directly from client-side code an intentional choice?"

Intentional choice, just for testing on client side, then it must migrate to the server side.

"if your clients accessed your custom APIs via API Gateway, and you would handle authentication, authorisation, and request validation there."

Mmmmm, can be, but if the authentication and authorization is done trough the API Gateway that calls a Lambda function, the execution of Lambda has a cost also.

Best regards, marcelo

profile picture
answered 10 months ago

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions