Using Cognito user for fine-grained access to DynamoDB table rows
Asked Answered
N

1

2

I'm struggling to get Cognito authorization working for fine-grained DynamoDB access control. This seems to be something that lots of people have problems with but there don't seem to be any solutions that I can see. I am using the C++ AWS SDK although I don;t think that's relevant.

Let's say I have a table "MyUsers" which has a Primary key consisting of both Partition Key and Sort Key. The Partition key is a unique value for each user (e.g. "AB-CD-EF-GH") - let's call it the "UserID". I want these users to log on using Cognito, then use Cognito to provide temporary credentials to each user to give access to all the rows in the table that have their UserID as the Partition key (GetItem, PutItem, Query, etc), but not be able to access any rows that start with a different user's partition key.

This seems to be what is being described here: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_dynamodb_items.html

So I have set up a Cognito User Pool and Identity Pool. Adding a "User" with a UserID of "AA-AA-AA-AA" consists of two parts:

  1. Add a row to the MyUsers table, with PK = AA-AA-AA-AA (and SK = AA-AA-AA-AA) (There will be other rows in the table where the UserID is AA-AA-AA-AA but the SK represents something different. For now though I am only testing this with one row per user).
  2. Create a Cognito user with a user_name of AA-AA-AA-AA (The user can log on with this UserID or the preferred_name alias, together with a password).

This all works and Cognito correctly generates the relevant emails with verification codes, adds the user to the user pool, etc.

My Identity Pool is set up with the User Pool as an identity provider, and a Cognito_Role (which is what will give the logged-on Cognito user the permissions to read the table). This role appears under the Identity Pool as an authenticated role, "service-role/Cognito_Role". The role has a Trust Relationship as follows:

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": { "Federated": "cognito-identity.amazonaws.com" },
                "Action": [ "sts:AssumeRoleWithWebIdentity" ],
                "Condition": {
                    "StringEquals": {
                        "cognito-identity.amazonaws.com:aud": "[REGION]:[IDENTITYPOOL_GUID]"
                    },
                    "ForAnyValue:StringLike": {
                        "cognito-identity.amazonaws.com:amr": "authenticated"
                    }
                }
            }
        ]
    }

The role has a single policy attached, which is set up as follows:

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [ 
                    "dynamodb:GetItem",
                    "dynamodb:Query"
                ],
                "Resource": [ 
                    "arn:aws:dynamodb:[REGION]:[ACCOUNT]:table/MyUsers"
                ],
                "Condition": {
                    "ForAllValues:StringEquals": {
                        "dynamodb:LeadingKeys": [ "${cognito-identity.amazonaws.com:sub}" ]
                    }
                }
            }
        ]
    }

What this is SUPPOSED to do is allow Query / GetItem access on the MyUsers table ONLY if the UserID of the currently logged on Cognito user matches the Partition Key in the MyUsers table.

So for user AA-AA-AA-AA to query a table, the following process should happen:

  1. The user logs on using UserID and password (or preferred_name and password). This uses Cognito to log on using InitiateAuth. I note the IdToken from the response.
  2. When the user needs to access the database, I retrieve temporary credentials using GetId (using the IdToken retrieved above) GetTemporaryCredentials (again using the IdToken and the IdentityId returned by GetId) This correctly returns me a temporary Access Key, Secret Key and Session token.

I then create a DynamoDB client using the Access Key, Secret Key and Session token. I use this Client to perform a Query on the MyUsers table, using the UserID (AA-AA-AA-AA) as the PK and SK. But the Query request ALWAYS gives me an error response of:

FAILED: User: arn:aws:sts::[ACCOUNT]:assumed-role/Cognito_Role/CognitoIdentityCredentials is not authorized to perform: dynamodb:Query on resource: arn:aws:dynamodb:[REGION]:[ACCOUNT]:table/MyUsers because no identity-based policy allows the dynamodb:Query action

As part of my testing, I have tried removing the Condition section of the role's permissions policy completely. This correctly allows me to Query the row on the database (but obviously would not limit me to a specific Cognito user).

I also tried changing the policy Condition for the role to
"dynamodb:LeadingKeys": "AA-AA-AA-AA"
and this DOES allow access to this specific row for Query, but not for GetItem (I can live without GetItem access if necessary, although it would be good to get this working too if it's allowed).

However any attempt to try and use the "currently logged on Cognito user" has failed. I know that "sub" is an automatically-generated ID for each user, so I have set up the "attributes for access control" for the User Pool to "use default mappings", which maps username (i.e. my UserID, I hope) to a claim of 'sub'.

I tried various other things, in order to try and get this to work:

I tried replacing the "sub" in the policy Condition for the role to a specific Cognito user attribute, for example "dynamodb:LeadingKeys": [ "${cognito-identity.amazonaws.com:username}" ]
"dynamodb:LeadingKeys": [ "${cognito-identity.amazonaws.com:user_name}" ]
This gives exactly the same error response as above.

I have also added a custom attribute called "user_id" to the Cognito user, and when I create the user I copy the user ID into this attribute. Then I tried the following:
"dynamodb:LeadingKeys": [ "${cognito-identity.amazonaws.com:custom:user_id}" ]
I also tried adding "sts:TagSession" to the Trust relationship policy of the Role, and changing the role permissions policy to:
"dynamodb:LeadingKeys": "${aws:PrincipalTag/user_name}"
"dynamodb:LeadingKeys": "${aws:PrincipalTag/username}"
"dynamodb:LeadingKeys": "${aws:PrincipalTag/custom:user_id}"

But for every single thing that I have tried, I get exactly the same error message

FAILED: User: arn:aws:sts::[ACCOUNT]:assumed-role/Cognito_Role/CognitoIdentityCredentials is not authorized to perform: dynamodb:Query on resource: arn:aws:dynamodb:[REGION]:[ACCOUNT]:table/MyUsers because no identity-based policy allows the dynamodb:Query action

The only thing I have been able to find in my extensive searching for a solution is one mention that the 'sub' part of ${cognito-identity.amazonaws.com:sub} is not actually the Cognito UserID, but is actually the automatically-generated "Identity Pool" Id, and nothing to do with the User Pool. But if this is the case, then it seems that what I want to do (which seems like a not uncommon requirement, surely?) will mean using this Identity Pool Id as the PK for the MyUsers table. So all access to the table via my own UserID (AA-AA-AA-AA) will require adding an extra step to always retrieve an Identity Pool ID for the Cognito user AA-AA-AA-AA, and also use the Identity Pool as my partition Key (when there are reasons that I would like to use my own generated value (AA-AA-AA-AA) as the PK for the MyUsers table). Is there an easier way to achieve what I want? Or is there just no ability at all to link a Cognito User Pool username and a table Partition Key?

Any suggestions of things I might have missed along the way, or parts of this that I may have misunderstood (I am quite new to AWS) would be gratefully received! Thank you.

Neurology answered 12/7, 2023 at 16:43 Comment(0)
K
5

It is possible to use any Congnito User pool attribute (both standard and custom attributes) as a condition in the Identity Pool IAM Role in order to provide fine-grained access to your AWS resources (i.e. DynamoDB).

In your case you need to use the PrincipalTag (see details here) in your IAM Role policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Condition": {
                "ForAllValues:StringEquals": {
                    "dynamodb:LeadingKeys": [
                        "${aws:PrincipalTag/user_id}"
                    ]
                }
            },
            "Action": [
                "dynamodb:GetItem",
                "dynamodb:Query"
            ],
            "Resource": "arn:aws:dynamodb:[REGION]:[ACCOUNT]:table/[TABLE]",
            "Effect": "Allow"
        }
    ]
}

In order for this to work you should create an AWS::Cognito::IdentityPoolPrincipalTag resource where you provide a mapping between your User pool attributes (see Claim field) and the PrincipalTag's Tag Key:

enter image description here

In the example above you map the aws:PrincipalTag/user_id tag key to the custom User pool attribute (or claim) custom:user_id, which you can then use in your IAM policy.

You can also use the default attributes, see details here. So, for Cognito User pool you can use sub (User pool user id) and aud (User pool App client id) attributes (claims) to map to your tag key (i.e. user_id).

As for the following attributes you may find in AWS documentation:

  • ${www.amazon.com:user_id}
  • ${graph.facebook.com:id}
  • ${accounts.google.com:sub}
  • ${cognito-identity.amazonaws.com:sub}

these are the attributes from the respective Identity providers (Amazon, Facebook, Google, Cognito Identity Pool) in the Identity pool, and not in the User Pool.

For instance, ${cognito-identity.amazonaws.com:sub} refers to the sub attribute for an identity inside the Cognito Identity Pool, not Cognito User Pool. Its value looks like this:

eu-central-1:edfeed68-3f49-4e69-bd02-27484b04a4b6

If you go over to your Identity Pool in AWS Console and click Identity browser - you will find a list of identities, which are of course different from the User Pool identities (users): enter image description here

Keep in mind, that Cognito Identity Pool doesn't need a User Pool - it may, but in general it can exist on its own. Cognito User pool acts as just another Identity Provider (e.g. Google, Facebook, Amazon, etc.).


UPDATE

In order to create a IdentityPoolPrincipalTag mapping and find out which attribute you can create a mapping to, go to your User pool's Users tab:

enter image description here

Now click on any available user:

enter image description here

You can only use existing User pool attributes (default or custom)! From what you were trying - username, user_name, user name - none of those exists in the user attributes.

As you can see in the screenshot above, you have a default sub attribute (unique). Or you can create a custom attribute (see custom:g_uuid attribute), either manually in the AWS Console for each user or automatically using Cognito Lambda triggers (e.g. using Pre sign-up Lambda trigger).

K2 answered 2/8, 2023 at 7:1 Comment(7)
many thanks for your detailed explanation. It makes sense, although I had already tried using PrincipalTag but without much luck. I have tried this again, following through your explanations above, but still cannot get this working.Neurology
I want to set the PrincipalTag to the Cognito user's "User name" (i.e. the value that appears in the "User name" column when I click the "Users" tab for my Cognito User Pool). I have updated my IAM Role's Permissions Policy to use "${aws:PrincipalTag/user_id}" and set up the Identity Pool's "Attributes for Access control" using a Tag key of "user_id" - in the "Claim" box, I have tried "username", "user_name", "user name" - but none of these work. The "Trust relationship" for my IAM role does include "sts:TagSession". Thanks!Neurology
@Neurology what kind of Cognito user pool attribute do you want to map to your PrincipalTag - built-in or custom? If you need to map the PrincipalTag to the User Pool's user ID attribute, then set the Claim field to sub. See details here docs.aws.amazon.com/cognito/latest/developerguide/…K2
I want to use a built-in attribute, the "user name" - i.e. the unique (and immutable) user name that I use to create the Cognito user (and that they subsequently sign in with). It's NOT the "User ID" / "sub" (which is a Guid created for the user by Cognito)Neurology
On the dashboard, it's listed as the first column (and called "User name") when I list the users in the user pool. ThanksNeurology
User name(first column) is not a built-in attribute. So you can't use it. To view all the available attributes, click on the respective user in the AWS Cognito console . However, you can create a custom attribute, which has a similar format with the User name column. The way you do it is up to you, you can manually add this attribute in AWS Console, or you can automate it using Cognito Lambda triggers i.e. PostConfirmation_ConfirmSignUp. See details here docs.aws.amazon.com/cognito/latest/developerguide/…K2
maslick, thank you again for your time and for updating your answer. You're right, looks like the reason I couldn't do this is because the "User name" I want to use is not an 'attribute' as such. I have managed to achieve what I want by (as you suggest) creating a "custom:user_id" attribute, which is a direct copy of the "User name" - and then I set the "Claim" field to custom:user_id and map this to a "user_id" tag which I then use as the PrincipalTag. This seems to work even though it's duplicating information (which I don't like to do...!).Neurology

© 2022 - 2024 — McMap. All rights reserved.