Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to find the bidirectional map between Cognito identity ID and Cognito user information? #54

Open
baharev opened this issue Dec 10, 2017 · 170 comments
Assignees
Labels
Auth Cognito feature-request Service Team

Comments

@baharev
Copy link
Contributor

@baharev baharev commented Dec 10, 2017

Given the Cognito identity ID, I would like to programmatically find the user name, e-mail address, etc. For example, one issue is that each user gets his/her own folder in S3 (e.g. private/${cognito-identity.amazonaws.com:sub}/ according to the myproject_userfiles_MOBILEHUB_123456789 IAM policy) but I cannot relate that folder name (S3 prefix) to the user attributes in my user pool. The closest thing that I have found is this rather complicated code:

AWS Lambda API gateway with Cognito - how to use IdentityId to access and update UserPool attributes?

Is it my best bet? Is it really this difficult?

(As a workaround, I would be happy with a post confirmation lambda trigger that creates for example a ${cognito-identity.amazonaws.com:sub}/info.txt file in some S3 bucket, and in the info.txt file it could place the user sub from the user pool. I am not sure that this is feasible at all, it was just an idea.)

@mlabieniec mlabieniec added investigating feature-request labels Dec 12, 2017
@mlabieniec mlabieniec added this to Icebox in aws-amplify via automation Dec 14, 2017
@jonsmirl
Copy link

@jonsmirl jonsmirl commented Jan 5, 2018

When the identity is created in the identity pool, can't you add 'logins' to the call? Those logins get stored in the identity pool providing a way to map between Google/Facebook/User Pool ID and the Identity pool ID. 'Logins' are optional, but they are very useful.

--logins (map)
A set of optional name-value pairs that map provider names to provider tokens.

Edit - you have the logins in Auth.ts/setCredentialsFromFederation.
Why aren't these getting stored in the identity pool along with the ID from federation, I'm sure that data used to be in my identity pools, so where did it go?

Here's an identity pool entry from my mobile app, it has the User Pool ID in it. The mobile app is based on code from Mobile Hub.

jonsmirl@ubuntu-16:~$ aws cognito-identity describe-identity --identity-id us-east-1:bea3f57c-4564-47cf-b82c-a515f719d8a5 --profile bill
{
"Logins": [
"cognito-idp.us-east-1.amazonaws.com/us-east-1_Cf6AJpe20"
],
"LastModifiedDate": 1512005509.277,
"CreationDate": 1512005509.237,
"IdentityId": "us-east-1:bea3f57c-4564-47cf-b82c-a515f719d8a5"
}

I checked Google and FB logins and they don't display the Google/FB sub like the user pool entry does.

So my mobile app is storing the User Pool ID. This seems to be missing from amplify. If that data was in the identity pool this issue would be solved.

In Auth.ts there is this code:
private setCredentialsFromFederation(provider, token, user) {
const domains = {
'google': 'accounts.google.com',
'facebook': 'graph.facebook.com',
'amazon': 'www.amazon.com'
};

I wonder if that string is fixed format? Maybe it is legal to say...
'google': 'accounts.google.com/34455444444',

ie add in the Google sub id? That appears to be what the mobile hub code did for Cognito.

@mlabieniec mlabieniec removed the investigating label Jan 11, 2018
@richardzcode
Copy link
Contributor

@richardzcode richardzcode commented Jan 27, 2018

Hi @baharev lookup user attributes by identityId is application specific. Some app may not want this for security concerns, some want to store in S3, and some prefer database. As a library we can't make those assumptions.

With aws-amplify it is pretty easy to implement. Depend on your needs, maybe pick one of the below two implementation.

Save private so only owner of the info can access

When user sign up, or maybe sign in depend on your choice,

  Auth.currentUserInfo()
    .then(info => {
      Storage.put(
        'userInfo.json',
        JSON.stringify(info),
        { level: 'private', contentType: 'application/json' }
      );
    });

Then do this to load user attributes,

  Storage.get('userInfo.json', { level: 'private',  download: true})
    .then(data => console.log('info...', data.Body.toString()));

Save public so just lookup by identityId

  Auth.currentUserInfo()
    .then(info => {
      Storage.put(
        identityId + '_userInfo.json',
        JSON.stringify(info),
        { level: 'public', contentType: 'application/json' }
      );
    });

Then do this to retrieve user attributes,

  Storage.get(identityId + '_userInfo.json', { level: 'public',  download: true})
      .then(data => console.log('info...', data.Body.toString()));

@baharev
Copy link
Contributor Author

@baharev baharev commented Jan 27, 2018

@richardzcode Thanks, your first suggestion is an acceptable workaround for the time being. What I don't like about it is that the userInfo.json in your code comes from the user, and therefore cannot be trusted.

lookup user attributes by identityId is application specific.

There is a misunderstanding here: I need this bidirectional map purely on the backend. The user must not be able to access it exactly for security reasons.

The use cases are the followings.

  1. If a user is complaining about a bug, I have to be able to find his/her folder to look at the data and to reproduce the bug (if any). In this use case I know the user but I don't know which folder is his/her folder.
  2. Say I noticed something strange in certain folders of the S3 bucket, and I would like to reach the corresponding users in e-mail and warn them. In this use case I know the folder but I don't know the users' e-mail address.

Note that both use cases must happen purely on the backend, and without any interaction from the user.

The true solution would be to retrieve this bidirectional map purely on the backend, without any user interaction. I think there should be an AWS API for backend code to do that. In my first comment I link to a horribly complicated "solution", but I find that unacceptably complex.

@baharev
Copy link
Contributor Author

@baharev baharev commented Jan 27, 2018

I looked at the Cognito API reference, and it is weird too. For example:

  1. AdminGetUser does not seem to give me the identity ID.
  2. DescribeIdentity does not seem give me any user attributes.
  3. Somehow the mapping between users and identity IDs seem to be hidden. The only way I could recover it is through the login tokens.

What is going on here? Or what did I miss / misunderstand?

@richardzcode
Copy link
Contributor

@richardzcode richardzcode commented Feb 1, 2018

@baharev In my knowledge there is no backend direct mapping between identityId and username. Backend is depend on what services provide. We can only suggest client side solution at this moment.

@jonsmirl
Copy link

@jonsmirl jonsmirl commented Feb 1, 2018

The old Mobile Hub code for Android built a two way map. This Indentity pool entry was created by that code when I did a user pool login:

jonsmirl@ubuntu-16:~$ aws cognito-identity describe-identity --identity-id us-east-1:bea3f57c-4564-47cf-b82c-a515f719d8a5 --profile bill
{
"Logins": [
"cognito-idp.us-east-1.amazonaws.com/us-east-1_Cf6AJpe20"
],
"LastModifiedDate": 1512005509.277,
"CreationDate": 1512005509.237,
"IdentityId": "us-east-1:bea3f57c-4564-47cf-b82c-a515f719d8a5"
}

Also, if you use the current hosted UI under user pool it creates a back link like above. It is only Amplify that is not creating the back link.

I just used Amplify to create an entry in my identity pool corresponding to a userpool entry. The entry in the identity pool does not have the linked login info shown above.

jonsmirl@ubuntu-16:~/aosp/demo/foobar/client$ aws cognito-identity describe-identity --identity-id us-east-1:f763ea29-41ce-4b86-bc58-31de68d7cce8
{
"LastModifiedDate": 1517513923.798,
"CreationDate": 1517513923.798,
"IdentityId": "us-east-1:f763ea29-41ce-4b86-bc58-31de68d7cce8"
}

@baharev
Copy link
Contributor Author

@baharev baharev commented Feb 1, 2018

@richardzcode This map must exist on the backend, because a given user always gets the same ${cognito-identity.amazonaws.com:sub}. It must be solvable purely on the backend.

@baharev
Copy link
Contributor Author

@baharev baharev commented Feb 1, 2018

@jonsmirl Sorry, I don't understand, but it seems to me that you are also struggling with this issue, or with a very similar one.

@jonsmirl
Copy link

@jonsmirl jonsmirl commented Feb 1, 2018

When amplify creates an entry in identity pool corresponding to a user pool entry, why is this part missing? Other Amazon Cognito code makes this entry:

"Logins": [
"cognito-idp.us-east-1.amazonaws.com/us-east-1_Cf6AJpe20"
],

That entry lets you map from an identity pool ID back into a user pool ID. You can query the logins off from an identity pool entry to get that string. Then use that string to query all of the attributes for the user out of the user pool.

@baharev
Copy link
Contributor Author

@baharev baharev commented Feb 1, 2018

@jonsmirl OK, now we are getting somewhere. Please explain:

Then use that string to query all of the attributes for the user out of the user pool.

How? Which API call will consume this mysterious "cognito-idp.us-east-1.amazonaws.com/us-east-1_Cf6AJpe20" and give me the user attributes?

@jonsmirl
Copy link

@jonsmirl jonsmirl commented Feb 1, 2018

We don't use this feature, and I see now that I was misunderstanding what I was seeing. This is the Cognito user pool id, not the sub of the user: cognito-idp.us-east-1.amazonaws.com/us-east-1_Cf6AJpe20
I was thinking that last part was the sub of the user in the pool and it is not.

You would need to be able to access the token that was submitted with this identity and I don't see any obvious way to get to it. 99% of our logins are via Google/FB with only a few using User Pool.

So to make this work, after you get logged in you will need a dynamodb table that maps the cognito identify id to the cognito user id. Then you will be able to find the user in the user pool.

Or you will just end up doing what we did. Since the attributes on Google, FB and user pool are all different we just store a copy of the attributes we care about in our internal user database. Which is what richard said to do.

This might be what you are looking for:

https://stackoverflow.com/questions/42386180/aws-lambda-api-gateway-with-cognito-how-to-use-identityid-to-access-and-update

@baharev
Copy link
Contributor Author

@baharev baharev commented Feb 1, 2018

@jonsmirl Thanks for the prompt reply.

As I said, yes, it would be an acceptable workaround for the time being, but it requires a user login and the info comes from the user (hence cannot be trusted). What pisses me is that this map must be available in the backend, so I see no reason why the user pool owner cannot access it. It just does not make sense to me, and I am wondering why the others aren't complaining about it too.

This might be what you are looking for:

https://stackoverflow.com/questions/42386180/aws-lambda-api-gateway-with-cognito-how-to-use-identityid-to-access-and-update

That is the link in my very first comment. :) I know you can do it, but it is unacceptably complex.

Thanks again for the prompt feedback!

@WolfWalter
Copy link

@WolfWalter WolfWalter commented Feb 25, 2018

Totally agreeing with @baharev. In my use case I have stored the Cognito Identity IDs in my database and now tried to get the username of the associated userpool user. None of the APIs seems to expose this functionality.

@ffxsam
Copy link
Contributor

@ffxsam ffxsam commented Mar 4, 2018

I was advised to retrieve the Cognito identity ID via Auth.currentUserInfo() and store it as an attribute in the user object in the user pool.

@ffxsam
Copy link
Contributor

@ffxsam ffxsam commented Mar 4, 2018

However, I can't figure out when the right time is to grab this info. Upon login, when I call Auth.currentUserInfo(), the id property is undefined.

  async signIn({ commit }, { username, password }) {
    const user = await Auth.signIn(username, password);
    const userInfo = await Auth.currentUserInfo();
    console.log(userInfo);
    authenticate(commit, user);
  },

@ffxsam
Copy link
Contributor

@ffxsam ffxsam commented Mar 4, 2018

Ok, this works for me:

  async signIn({ commit }, { username, password }) {
    const user = await Auth.signIn(username, password);
    const credentials = await Auth.currentCredentials();
    console.log('Cognito identity ID:', credentials.identityId);
    authenticate(commit, user);
  },

So within this code block, you could just update the user's attributes and set their Cognito identity ID in there, and you'd have immediate access to it once they're authenticated.

@baharev
Copy link
Contributor Author

@baharev baharev commented Mar 4, 2018

@ffxsam Interesting, thanks for letting us know. I recognize Auth.signIn() and Auth.currentCredentials() but could you explain what authenticate is in your code?

If I understand correctly what you are suggesting, it is essentially the same as richardzcode's suggestion. See my previous objections why I am not happy with it as a solution. It is an acceptable workaround for the time being though.

@ffxsam
Copy link
Contributor

@ffxsam ffxsam commented Mar 5, 2018

authenticate isn't relevant here, just my own function that commits data to a Vuex store.

@gbrits
Copy link

@gbrits gbrits commented Mar 13, 2018

This is how I overcame the obstacle of not having the identityId on hand for my users:

login() {
    let loading = this.loadingCtrl.create({
      content: 'Please wait...'
    });
    loading.present();

    let details = this.loginDetails;
    Auth.signIn(details.username, details.password)
      .then(user => {
        Auth.currentCredentials().then(credentials => {
          var identityId = credentials.identityId;
          let params = {
             "AccessToken": user.signInUserSession.accessToken.jwtToken,
             "UserAttributes": [
                {
                   "Name": "custom:identityId",
                   "Value": identityId
                }
             ]
          }
          // Save the identityId custom attribute to the user
          this.db.getCognitoClient()
          .then((client) => {
            client.updateUserAttributes(params, function(err, data) {
                if (err) console.log(err, err.stack);// an error occurred
                else {
                  console.log(data);
                }
            });
          });
        });
        logger.debug('signed in user', user);

        if (user.challengeName === 'SMS_MFA') {
          this.navCtrl.push(ConfirmSignInPage, { 'user': user });
        } else {
          this.navCtrl.setRoot(TabsPage);
        }
      })
      .catch(err => {
        logger.debug('errrror', err);
        this.error = err;
      })
      .then(() => loading.dismiss());
  }

Ref: inside of my DynamoDB (public db) class, my getCognitoClient method invokes the CognitoIdentityServiceProvider endpoint from the aws-sdk

getCognitoClient() {
      return Auth.currentCredentials()
        .then(credentials => new AWS.CognitoIdentityServiceProvider({ credentials: credentials }))
        .catch(err => logger.debug('error getting document client', err));
  }

Important Note You have to log into Cognito's User Pool and click Attributes and add IdentityId (as a string) to your custom attributes, for it to be populated.

Hope this helps someone. Because it had me thinking I'm an idiot who should stop programming, so hopefully someone out there can reassure me to continue!

@baharev
Copy link
Contributor Author

@baharev baharev commented Mar 17, 2018

@gbrits I think that we should get official support through the SDK, and we really should not be implementing our workarounds. Especially not in ways that involve data coming from the user (untrusted data).

As far as I understand your code, it has the same issues as richardzcode's workaround.

@ffxsam
Copy link
Contributor

@ffxsam ffxsam commented Mar 18, 2018

Totally agree. The SDK should handle these sorts of lower level operations for us.

@v1pz3n
Copy link

@v1pz3n v1pz3n commented Jul 24, 2021

@wvidana I used it in the same way mentioned by @KeKs0r , directly in the put method.

I only have one screen that sends to S3, I didn't see the need to do it globally

But in your case, take a look using the StorageProvider
https://docs.amplify.aws/lib/storage/getting-started/q/platform/js#using-a-custom-plugin
https://github.com/aws-amplify/amplify-js/blob/a047ce73/packages/storage/src/Storage.ts

@hkjpotato
Copy link
Contributor

@hkjpotato hkjpotato commented Jul 30, 2021

@JesseDavda @v1pz3n Thank you for providing the go around. I am from Amplify and I am thinking of supporting this approach directly in our tool. At this moment, we are trying to understand more about the use cases to ensure we don't miss some edge cases.

What you are proposing this, is the newly launched attribute-based access control (ABAC), which bridges the gap of "what we know about user"(e.g. user pool attribute) and "what the resource is identified by"(in IAM policy access control).

Can you (or anyone interested in this topic) give me a little more information about the use cases that you are trying to solve? Historically, we are using identityId as identifier for resource (not just S3), and it is because ${cognito-identity.amazonaws.com:sub} is the recognized variable by IAM.

And yes there is no API to get identityId from a user attribute. You can call getId with the logins:idToken. This is what we do in amplify when set the AWS credentials.

Another potential solution is to make your cognito user pool as a developer idp, then you can use LookupDevleoperIdentity, but it is not very convenient.

As mentioned above, one solution is to store identityId as a custom attribute, but I am not sure how that helps address the use case of "given an identity Id, I want to find the related user" easily because according to the listUser api you cannot filter based on custom attribute.

My primary concern on using user pool attribute as identifier is the guest user use case. As you probably noticed, user pool does not support guest user, and currently we rely on identity pool to give us guestIdentity which can be used that as an identifier for the guest resource. Is this the use case you want to support as well?


after reading more on the past comments so far these are the use cases I know

  1. for a given resource under certain identifier, I want to know who is the user (from user pool) that owns it.
  2. for a given user, I want to find its related resource.
  3. in certain lambda function I want to be able to access/relate the user with the given context.

I am not sure if "sub" is a good replacement for identity. Given a "sub" how do you normally find the user from user pool?

@dorontal
Copy link

@dorontal dorontal commented Jul 30, 2021

The main use case for me is simple: it does not make sense to keep track of more than one id per user.

I much prefer to only use one ID everywhere per user. Having to deal with only one ID per user makes the code much clearer, neater and simpler.

One reason for this preference is cost - it costs more to keep two IDs per user. Cost = more development time and complexity, more points of failure in the system, more money if your app has many users - and that's something that probably many app developers dream about.

Also, developers by nature often are concerned about efficiency - so having to keep track of more than one ID per user is going to cause more developers to be more disgruntled with the inefficiencies and costs forced upon them by the library.

My use case for S3 authentication is exactly as it was originally intended and described, with the three levels private / public / protected -- you have done a nice job IMO with that setup -- it's just nicer and cheaper not to have to deal with two IDs -- for me it's a simple as that.

So, to answer your question, there's no major new use case for me.

To answer other question "Given sub, how do we find the user from the user pool?":

I keep a User model in the GraphQL schema whose primary key is "sub", as follows

    type User
      @auth(
        rules: [
          { allow: owner, ownerField: "sub" }
        ]
      )
      @key(fields: ["sub"]) {
      sub: String!
      ...
    }

in other words there is not the usual id: ID primary key for the user but instead I call it "sub" -- if you use any @auth directives in this User model now, you will need to specify the ownerField to be that sub.

Now if I need any info about the user I just retrieve it via sub from the DB...

With this setup, there is never a need to retrieve the user from the user pool -- all the info you'll ever need about the user is in the DB.

@hkjpotato
Copy link
Contributor

@hkjpotato hkjpotato commented Aug 2, 2021

@dorontal Thank you, and I agree with you on maintaining single source of truth.
If I understand it correctly, you are maintaining a separate database (other than the user pool itself), for accessing user object? Will it be easy for you to also keep track of identityId in that database?

@dorontal
Copy link

@dorontal dorontal commented Aug 2, 2021

@hkjpotato, it would not be useful for me to also keep track of identityId in the DynamoDB database.

This is because - after having implemented the solution outlined by @JesseDavda as I outlined above - I do not need or use the Cognito identityId at all anymore.

@hkjpotato
Copy link
Contributor

@hkjpotato hkjpotato commented Aug 2, 2021

@dorontal I see, do you see any issue of using ${aws:PrincipalTag/cognitoId} directly in Resource field of IAM policy? In my local testing, it seems this variable can only be used in Condition field.

Besides, do you use any AWS resource like "AWS_Gateway" and use "AWS_IAM" as Authorizer?

@JesseDavda @baharev Can you help me understand more about the "no identity id" use case? Basically I am trying to see if we switch to "attribute based access control" and promote using "sub" user attribute to identify resource:

  • Can we still use IAM role for authorization (e.g. still use AWS_IAM as Authorizer) and can get enough context from API_GATEWAY/Lambda event? Or do we need to switch to use "Cognito User Pool" as Authorizer.
  • Is there any other cases where we have to get identityId.

Thank you in advance! The information you provide can really help us stay on the right track.

@dorontal
Copy link

@dorontal dorontal commented Aug 8, 2021

I have had no problems using ${aws:PrincipalTag/cognitoId} directly in the resource field of the IAM policy.

I also tried to substitute ${aws:PrincipalTag/username} instead of ${aws:PrincipalTag/cognitoId}, as suggested by @KeKs0r and this substitution also worked identically to the one above.

@hkjpotato - did you follow the steps I outlined precisely as stated? I did, just now, test those steps again, and they do work for me precisely as stated.

@v1pz3n
Copy link

@v1pz3n v1pz3n commented Aug 14, 2021

@hkjpotato my case is exactly the same as the @dorontal

I prefer not to have to manage with multiple identities, because my system only needs to serve one visitor on the authentication page, remember password and both only use pinpoint.

After it is authenticated, I use the sub in appsync, s3, dynamodb, ...

I prefer that Cognito solves this and my application recognizes a single user.

But in this discussion I ended up having a question, if you prefer the identityid approach, why use the sub in the datastore?

@dn-l
Copy link

@dn-l dn-l commented Aug 28, 2021

Theoretically, /protected/${cognito-identity.amazonaws.com:cognito:username}/* should work without changing IAM because it's a built-in claim but it doesn't ... no idea why.
Suggested solutions didn't work for me, getting 403 on PutObject

@shadow-light
Copy link

@shadow-light shadow-light commented Oct 5, 2021

For those using the builtin Cognito user pool username field, the mapping needs to be username -> cognito:username, as sub is just a uuid.

@v1pz3n
Copy link

@v1pz3n v1pz3n commented Oct 17, 2021

I had to recreate the userpool and this workaround stopped working.

${aws:PrincipalTag/username}

Apparently the cognito team made updates to the product and we didn't know about it.

@shadow-light
Copy link

@shadow-light shadow-light commented Oct 17, 2021

You probably have to enable "Attributes for access control" in the identity pool again as it is tied to a specific user pool (the one you deleted)?

@v1pz3n
Copy link

@v1pz3n v1pz3n commented Oct 17, 2021

@shadow-light thank you

i just realized this, i forgot to put this on cdk.
I should have seen your comment as it would have saved me 30 minutes ;)

@danielkochdakitec
Copy link

@danielkochdakitec danielkochdakitec commented Oct 25, 2021

@dorontal Do we need to persist the changes to the role and to the identity pool in the cloud formation template? If not, then would that mean we need to do that manually?

@dorontal
Copy link

@dorontal dorontal commented Oct 25, 2021

@danielkochdakitec The change to the role (in my step #1 above) was done via the console and therefore it was automatically persisted - there was no further save action required - once you make changes via the console, they remain there. The change to the identity pool (in step #2 above) was also made via the console and therefore automatically saved.

@danielkochdakitec
Copy link

@danielkochdakitec danielkochdakitec commented Oct 26, 2021

@dorontal Thank you very much! I was just not sure what happens, when I want to deploy my amplify project to another account: Then I would need to do these changes again I guess. That's why I wanted to persist them.

Regarding your note I can tell you that it still works for me. I implemented the workaround yesterday and it worked correctly.

@dorontal
Copy link

@dorontal dorontal commented Oct 26, 2021

Thank you for confirming this workaround still works. Just tested it as well and I can attest to the same conclusion (removed the doubts from my comment above so as not to confuse).

@jleskovar
Copy link

@jleskovar jleskovar commented Nov 30, 2021

While it's great there's a workaround, I don't think this is acceptable long-term. Re the question of "persistence" from @danielblignaut , it's still a valid question. I'm not sure how to persist the changes to our code repository - not sure if you've had any thoughts on this @dorontal ? The latest version of amplify-cli basically adds all of the cloudformation JSON directories (amplify/backend/.../build) to gitignore. This means if we need to create a new env, or deploy to a completely different account, all of these manual hacks need to be re-applied. The quick litmus test is: perform the changes directly in the web console, and perform an amplify pull. The manual tweaks don't get pulled down - even worse, the "#current-backend" directory still references the old config, so there is configuration drift that isn't being managed anywhere. Also, performing the global replace on the JSON files in the repo, and performing a amplify push, doesn't detect that there are changes to apply

@jleskovar
Copy link

@jleskovar jleskovar commented Dec 1, 2021

After spending about a day trying to get the workaround working - I think I'm going to have to admit defeat and try something else. The problem for me is that - at least in the newer versions of CLI (7.5.5 as of writing) - there is no way to modify those CF templates within the Git repo. Making the changes to the JSON files in the repo has no effect. Even after manually making the edits through the console, a subsequent amplify push reverts everything back. I've tried modifying the CF stack templates directly through the console as well to no avail. I think the intended way to do this is via the custom CDK stuff, but sadly I don't have a lot of appetite to learn yet another config language, with a dubious chance of success. I've burnt far too much time on this problem when I should be spiking out my app.

Thinking about this issue more holistically - I think this should be solved within the amplify tooling itself. The fact that the Amplify platform has two different notions of "User Identity" (Identity ID and Cognito User Pool sub) that are used inconsistently across the different APIs (Storage and AppSync API specifically) is a pretty fundamental bug. What makes sense to me would be to incorporate the changes above as the default behaviour for new stacks going forward, with some sort of migration path to having a unified user ID (for e.g., by adding some sort "StorageV2" API that uses the PrincipalTag in the path for protected and private files, so it matches authenticated AppSync models)

@ivadenis
Copy link

@ivadenis ivadenis commented Jan 23, 2022

@hkjpotato is amplify team still looking into this?

@dorontal
Copy link

@dorontal dorontal commented Jan 27, 2022

I now believe that most likely the workaround here conflicts somehow with the sign-in process. It did not conflict with the sign in process before, at some point, but now it does.

As you can see from this issue: aws-amplify/amplify-flutter#1204 -- adding attributes for access control (in this case adding the maaping cognitoId -> sub) seems to break Cognito token federation during authentication, as reported in the issue aws-amplify/amplify-flutter#1204

It would be nice if we could still use "adding attributes for access control" so that the workaround in this issue works, but currently it does not.

@acrolink
Copy link

@acrolink acrolink commented May 5, 2022

@v1pz3n
You deserve an award for that solution! Thank you.

Hope it does not break other things related to authentication.

@johnf
Copy link

@johnf johnf commented May 31, 2022

I've managed to get this working and mostly automated by making the following changes

First update the auth roles to use${aws:PrincipalTag/username}`` in amplify/backend/storage/YourStorage/override.ts```

Note: I've only updated for protected since it doesn't matter for private. I've left the code for that below but I hit a bug when enabling it where the override wouldn't work anymore and ran out of time to solve it.

import { AmplifyS3ResourceTemplate } from '@aws-amplify/cli-extensibility-helper';

export function override(resources: AmplifyS3ResourceTemplate) {
  resources.s3AuthProtectedPolicy.policyDocument.Statement = resources.s3AuthProtectedPolicy.policyDocument.Statement.map((statement) => ({
    ...statement,
    Resource: JSON.parse(JSON.stringify(statement.Resource).replace('${cognito-identity.amazonaws.com:sub}', '${aws:PrincipalTag/username}')),
  }));

  // NOTE: We don't need these for our app and also they don't work
  // resources.s3AuthPrivatePolicy.policyDocument.Statement = resources.s3AuthPrivatePolicy.policyDocument.Statement.map((statement) => ({
  //   ...statement,
  //   Resource: JSON.parse(JSON.stringify(statement.Resource).replace('${cognito-identity.amazonaws.com:sub}', '${aws:PrincipalTag/username}')),
  // }));
  //
  // resources.s3AuthReadPolicy.policyDocument.statements = resources.s3AuthReadPolicy.policyDocument.statements.map((statement) => ({
  //   ...statement,
  //   condition: JSON.parse(JSON.stringify(statement.condition).replace(/\${cognito-identity.amazonaws.com:sub}/g, '${aws:PrincipalTag/username}')),
  // }));

  // console.debug(JSON.stringify(resources.s3AuthProtectedPolicy.policyDocument.Statement, null, 2));
  // console.debug(JSON.stringify(resources.s3AuthPrivatePolicy.policyDocument.Statement, null, 2));
  // console.debug(JSON.stringify(resources.s3AuthReadPolicy.policyDocument.statements, null, 2));
}

I added a post push hook to set up the principal tag attributes, you can't do this in an override using cdk as cloudformation doesn't support it yet. aws-cloudformation/cloudformation-coverage-roadmap#779

const fixPrincipalTags = async (data, error) => {
  console.log('ease');
  if (error) {
    console.error('Error setting up sentry release', error);
    return;
  }
  const env = data.amplify.environment.envName;
  process.env.AWS_PROFILE = `gladly-${env}`;

  const client = new CognitoIdentity({ region: 'ap-southeast-2' });

  const credentials = await client.config.credentials();
  process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
  process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;

  // NOTE: Change mobile to the name of your auth in amplify
  const identityPoolId = amplify.auth.mobile.output.IdentityPoolId;
  const identityPool = await client.describeIdentityPool({ IdentityPoolId: identityPoolId });
  const identityProviderName = (identityPool.CognitoIdentityProviders || [null])[0]?.ProviderName;

  try {
    const tagMap = await client.getPrincipalTagAttributeMap({ IdentityPoolId: identityPoolId, IdentityProviderName: identityProviderName });
    if (tagMap?.PrincipalTags?.client === 'aud' && tagMap?.PrincipalTags?.username === 'sub') {
      console.debug('No tags to fix');
      return;
    }
  } catch (e) {
    console.debug('Fixing tags');
    await client.setPrincipalTagAttributeMap({
      IdentityPoolId: identityPoolId,
      IdentityProviderName: identityProviderName,
      UseDefaults: true,
    });
  }

  delete process.env.AWS_PROFILE;
};

This most important change and why I suspect this workaround stopped working for people is changing the trust relationship on the auth role.

const addStsSessionTag = async (data, error) => {
  if (error) {
    console.error('Error:', error);
    return;
  }
  const env = data.amplify.environment.envName;
  process.env.AWS_PROFILE = `gladly-${env}`;

  const client = new IAM({ region: 'ap-southeast-2' });

  const credentials = await client.config.credentials();
  process.env.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
  process.env.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
  process.env.AWS_SESSION_TOKEN = credentials.sessionToken;

  const roleName = amplify.providers.awscloudformation.AuthRoleName;

  const role = await client.getRole({ RoleName: roleName });
  const policyJSON = role.Role?.AssumeRolePolicyDocument;
  if (!policyJSON) {
    throw new Error(`Role ${roleName} does not have an AssumeRolePolicyDocument`);
  }
  const policy = JSON.parse(decodeURIComponent(policyJSON));
  const actions = policy.Statement[0].Action;

  if (actions.includes('sts:TagSession')) {
    console.debug('Role already has sts:TagSession');
    return;
  }

  actions.push('sts:TagSession');

  console.debug('Adding sts:TagSession to policy');
  await client.updateAssumeRolePolicy({ RoleName: roleName, PolicyDocument: JSON.stringify(policy) });
};

@Guneetgstar
Copy link

@Guneetgstar Guneetgstar commented May 31, 2022

Ok, so after dwelling ~day and reading the same page again and again I found this:

If you are using developer authenticated identities, Amazon Cognito includes an API for looking up Cognito identity IDs for your users to make this process easier.

And now it makes sense to me at least that why, JUST WHY they added examples of Limiting Access to Specific Identities when there is no way to even find the ID from the backend/without involving users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Auth Cognito feature-request Service Team
Projects
No open projects
Development

No branches or pull requests