aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.41k stars 2.11k forks source link

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

Open baharev opened 6 years ago

baharev commented 6 years ago

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.)

baharev commented 5 years ago

@lestephane Thanks for the detailed explanation. Your use case is different from mine. In your case, you can get away with logged in / not logged in decisions and API keys.

In my case it is not possible; the users must have their own private data. Implementing this without IAM roles would mean re-implementing something that IAM gives me for free (if the user can assume IAM roles).

Thanks for the explanation, now I see that your use case is different from mine.

baharev commented 5 years ago

This has become the most commented open issue, and the second most commented issue of all times. I rest my case.

lestephane commented 5 years ago

@baharev Thanks for confirming that i don't need identity pools, making this issue irrelevant for me. Of course, I can't speak for anyone else following this issue. I was merely trying to find a way out of the dichotomy.

baharev commented 5 years ago

@lestephane There are valid use cases where you do not need IAM roles, and apparently your use case is such a use case. That's 100% OK. However, the situation may quickly change in the future if your requirements change, and then this issue will become relevant for you as well.

lestephane commented 5 years ago

Indeed @baharev I will continue following this issue

rjbaker commented 5 years ago

This scenario renders AWS Amplify pretty useless if you want to sign users in using Cognito user pools and then have them interact with a Lex bot. The SDK just passes their identity/IAM token, and without any way to map a user pool id with an identity id, you can't personalise any chat bot response for an authenticated user. Seems particularly short sighted.

The way I've worked round it is to hack the interactions function to pass the pool user id via chat session data, but this is technically insecure since the authenticated user could specify any user id and have the chat content personalised for their id.

thrixton commented 5 years ago

Here's a lambda function that resolves a userpool user or federated identity to an email address.
The function is using the serverless typescript template.
For the userPool, I do a listUsers request as per suggestion here.
For federation, I validate the token to ensure it's not been tampered with (not sure if CognitoIdentity validates it internally), then call CognitoIdentity.getId as per the suggestion above by @behrooziAWS.
Then, I compare the resolved identity to the request identity to ensure it's not a hijack attempt, if ok, I'll save the details into a DynamoDb table.

export const postAccountCurrentProfile: Handler = async (event: APIGatewayEvent, context: Context, cb: Callback) => {

  const body = JSON.parse(event.body);
  const provider = body.provider;

  const requestIdentityId = getCognitoId(event); //retrieve the id from the event
  var foundIdentityId: string;
  var foundEmail: string;

  if (provider === 'UserPool') {
    const sub = event.requestContext.identity.cognitoAuthenticationProvider.split(':CognitoSignIn:')[1];
    const request = {
      AttributesToGet: [], // NOTE: I'm using email as my username so don't have to get any attributes
      UserPoolId: process.env.user_pool_id,
      Filter: `sub = "${sub}"`,
      Limit: 1
    };
    const cognitoClient = new CognitoIdentityServiceProvider({ region: process.env.region });
    const users = await cognitoClient.listUsers(request).promise();
    foundEmail = users.Users[0].Username;
    foundIdentityId = requestIdentityId;
  } else if (provider === 'Google') {
    const gClient = new gApi.OAuth2Client(process.env.google_client_id);

    try {
      await gClient.verifyIdToken({ idToken: body.token, audience: process.env.google_client_id });
    } catch (x) {
      console.error(x);
      emptyResult(500, cb);
      return;
    }
    const identity = new CognitoIdentity();

    const params = {
      IdentityPoolId: process.env.identity_pool_id,
      Logins: {
        'accounts.google.com': body.token
      }
    };

    const response = await identity.getId(params).promise();

    foundIdentityId = response.IdentityId;
    foundEmail = token.email;
  }

  if (foundIdentityId !== requestIdentityId) {
    sLog.error('Account hijack attempt', { requestIdentityId, foundIdentityId });
    //TODO: send to sns queue
    emptyResult(500, cb);
    return;
  }

  okObjectResult({ identity: foundIdentityId, email: foundEmail }, cb);
};

Hope this helps someone.

baharev commented 5 years ago

This issue has become the most commented issue of all times (in this repo), and has 5 more days left to become 1 year old.

ffxsam commented 5 years ago

@hectomg What didn't you like about my solution above?

LeeIakokka commented 5 years ago

+1 for this feature request.

thrixton commented 5 years ago

@ffxsam , the problem with doing anything client side is you cannot trust the client. Anything coming from the client must be treated as potentially compromised and must be validated on the back-end against known good sources.

ffxsam commented 5 years ago

Fair point.

abirtley commented 5 years ago

Please add this feature. It is clearly already present in some fashion on the backend, as @baharev has explained. Ridiculous that it's taken this long and still no satisfactory resolution!

janpoltan commented 5 years ago

+1 for this. It is really hard to grab (probably no way) the identity id in the cognito post confirmation trigger when you use the cognito identity id as a leading key. (Though we could use sub, but we have a special use case for not using sub in the user attributes).

dylan-westbury commented 5 years ago

If identityId was available in the claims object for a lambda proxy, like the sub / userId is. All my problems would go away.

chadgarlandscg commented 5 years ago

+1 for this! I was happy to stumble across this and I'm surprised that it's still open.

My use case is to allow "host" users to upload "protected" images to S3, then let all types of users view the images when viewing a particular host. I couldn't find a way to attach the identity id to the hosts on the server side, since stateful authentication is required to retrieve the identity id, which only makes sense from the browser.

My understanding is that this is similar to or the same as what @baharev had to workaround:

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.

My problem is namely:

  1. AdminGetUser does not seem to give me the identity ID.

This isn't really an AWS Amplify issue though, since the fault seems to lie with the AWS SDK. I hope I'm not commenting on the wrong thread. 🙈

thrixton commented 5 years ago

@dylan-westbury & @chadgarlandscg , I'm a bit confused as to which IdentityId you're after. If you're after the IdentityPool IdentityId, this in a lambdaa event, this is available via the APIGatewayEvent.requestContext.identity.cognitoIdentityId property, the APIGEvent class is available in the lambda event. I can't find the docs for this class but it's listed at the below type link. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/index.d.ts

Apologies if I misunderstand :)

chadgarlandscg commented 5 years ago

@thrixton apologies for the late reply! I'm actually trying to do this in a Node server, not a Lambda.

My users ("guests" and "hosts") authenticate with Cognito via Amplify. Hosts can upload an image via Amplify, which saves the image under their protected folder in S3. Then, guests (authenticated or not) as well as other hosts can browse for hosts and see their saved image.

Hosts are registered via an admin API call, which signs them up with Cognito via the AWS SDK's adminCreateUser call. Neither this call, nor adminGetUser return the cognito identity id, which I need to store in my DB in order for all users to see the host's uploaded image in S3 via Amplify Storage. Therefore, I am forced (at least with my architecture) to rely on my web application to send up the cognito identity id on the initial host login.

Perhaps I am the one misunderstanding 🙈I'm so sorry if that's the case! My situation just seemed to closely mirror one of the original problems mentioned by @baharev here:

AdminGetUser does not seem to give me the identity ID.

Feel free to let me know if I'm doing something crazy or missing something obvious.

Thanks a bunch!!

TomiLahtinen commented 5 years ago

@dylan-westbury & @chadgarlandscg , I'm a bit confused as to which IdentityId you're after. If you're after the IdentityPool IdentityId, this in a lambdaa event, this is available via the APIGatewayEvent.requestContext.identity.cognitoIdentityId property, the APIGEvent class is available in the lambda event. I can't find the docs for this class but it's listed at the below type link. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/index.d.ts

Apologies if I misunderstand :)

Cognito Identity id can obtained from Lambda context yes, but it cannot be mapped to an actual user account in Cognito's user pool

dylan-westbury commented 5 years ago

@dylan-westbury & @chadgarlandscg , I'm a bit confused as to which IdentityId you're after. If you're after the IdentityPool IdentityId, this in a lambdaa event, this is available via the APIGatewayEvent.requestContext.identity.cognitoIdentityId property, the APIGEvent class is available in the lambda event. I can't find the docs for this class but it's listed at the below type link. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/index.d.ts

Apologies if I misunderstand :)

@dylan-westbury & @chadgarlandscg , I'm a bit confused as to which IdentityId you're after. If you're after the IdentityPool IdentityId, this in a lambdaa event, this is available via the APIGatewayEvent.requestContext.identity.cognitoIdentityId property, the APIGEvent class is available in the lambda event. I can't find the docs for this class but it's listed at the below type link. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/index.d.ts

Apologies if I misunderstand :)

Yes that is the correct identityId, but it's only available if the authorizer is set to AWS_IAM, as opposed to a cognito user pool authorizer

eceramanan commented 5 years ago

+1 for this feature request.

davekiss commented 5 years ago

Throwing my +1 in the ring here, I was pretty stunned to see this was even an issue given that Amplify leads you into this scenario ie. Storage module using Federated IDs but no way to relate them to User Pool IDs.

A simple extra field in one of the response or trigger payloads would seem to resolve this issue, no?

davekiss commented 5 years ago

Alright, I spent way too long trying to come up with a reasonable approach to linking the user's sub value from my Cognito User Pool to the IdentityId value from the Cognito Federated Id, and the solution I came up with isn't perfect, but it's somewhat sane and at least verifiable.

I noticed that requests to the AppSync GraphQL API's are authorized using the idToken value that is returned after a user authenticates via Cognito User Pools. ~That same token is also used in calls to the getId method of the AWS.CognitoIdentity JS SDK. (ahhaaaaaa)~

I decided to query the User's record that is stored within my custom DynamoDB User table based on the user's sub value that is returned after login. You can get this from the user claims object, ie.

   <Authenticator
      hideDefault={true}
      amplifyConfig={awsconfig}
      onStateChange={async authState => {
        if (authState === "signedIn") {
            const user = await Auth.currentAuthenticatedUser();
            const { sub } = user.attributes;

            try {
              const user = await API.graphql(
                graphqlOperation(getUser, { id: sub })
              );

              console.dir(user);
            } catch (err) {
              console.log("error loading user...: ", err);
            }
        }
   >

Then, I wrote a custom lambda resolver for the federatedId field on my User schema that will fetch the DynamoDB record's federatedId field if it isn't empty. If it is empty, I then use the authorization token which has already been verified as valid by AppSync and send it to the AWS.CognitoIdentity.getId method.

The resolver (Query.federatedId.req.vtl):

{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "type": "Query",
        "field": "federatedId",
        "arguments": $utils.toJson($context.arguments),
        "identity": $utils.toJson($context.identity),
        "source": $utils.toJson($context.source),
        "headers": $util.toJson($ctx.request.headers)
    }
}

The lambda body:

  // check your DynamoDB record here and only do the below
  // if your record doesn't already contain the federatedId
  // ...
  const { authorization } = event.headers;
  const { sub, iss, aud, token_use, email } = event.identity.claims;

  const userName = event.identity.claims["cognito:username"];

  if (iss !== `https://${ProviderName}`) {
    //fail
    callback("Unauthorized", null);
  }

  if (token_use !== "id") {
    //fail
    callback("Unauthorized", null);
  }

  if (aud !== clientId) {
    // fail
    callback("Unauthorized", null);
  }

  const params = {
    IdentityPoolId,
    Logins: {
      "cognito-idp.us-east-1.amazonaws.com/us-east-1_UsErPoOLiDLoL": authorization
    }
  };

  try {
    const result = await cognitoidentity.getId(params).promise();
    const { IdentityId } = result;
    // todo: update DynamoDB record with this info where User.id == sub
    // or User.userName === userName
    callback(null, IdentityId);
  } catch (err) {
    console.log(err);
    callback("Unable to resolve" + err, null);
  }

There's also a few sanity checks above to ensure the claims are valid against my stack.

const IdentityPoolId = "us-east-1:12345678-this-aint-real-abcdefghijkl";
const ProviderName = "cognito-idp.us-east-1.amazonaws.com/us-east-1_UsErPoOLiDLoL";
const clientId = "YOUR_ISSUER_CLIENT_ID";

It's still a bummer that this is even required, it requires a user login event, and, I'm technically making a mutation on a User query which is bad news, but it feels like a better approach than others.

The other approach I explored was using adminLinkProviderForUser on the User Pool Sign Up Confirmation trigger to proactively create the User's Federated ID, but the ProviderName value generated by Amplify is too long to pass the Preflight Validation performed by Cognito (limited to 32 characters) and this approach would also only work for new users, not existing users.

Hope this helps someone!

EDIT Upon further inspection, it looks like the idToken is only used as the authorization token in the AppSync Console and not in the amplify-js library. You'd have to also override the library authorization method in your Amplify.configure call in order to send the idToken instead of the accessToken

baharev commented 5 years ago

@davekiss Thanks for sharing. Unfortunately, your struggle only underlines my point: It is unnecessarily complicated and brings in other services and libraries (GraphQL, AppSync, DynamoDB, Lambda in your workaround).

I see no good technical reason why the Cognito REST API could not support it directly for the user pool owner.

keithdmoore commented 5 years ago

Pecked out from my iPhone

On Apr 7, 2019, at 1:46 PM, Ali Baharev notifications@github.com wrote:

@davekiss Thanks for sharing. Unfortunately, your struggle only underlines my point: It is unnecessarily complicated and brings in other services and libraries (GraphQL, AppSync, DynamoDB, Lambda in your workaround).

I see no good technical reason why the Cognito REST API could not support it directly.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

keithdmoore commented 5 years ago

After working with AWS and DynamoDB, it seems that management must be making technical decisions in order to increase revenue. Forcing these ugly workrounds which force you to use other services. DynamoDB can’t do some basic things without doing scans, etc

rudijs commented 5 years ago

Hi,

The serverless-stack.com have this in their tutorial:

"Mapping Cognito Identity Id and User Pool Id"

https://serverless-stack.com/chapters/mapping-cognito-identity-id-and-user-pool-id.html

Thoug it only at runtime inside a Lambda function.

export async function main(event, context, callback) {
  const authProvider = event.requestContext.identity.cognitoAuthenticationProvider;
  // Cognito authentication provider looks like:
  // cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxxxxxxx,cognito-idp.us-east-1.amazonaws.com/us-east-1_aaaaaaaaa:CognitoSignIn:qqqqqqqq-1111-2222-3333-rrrrrrrrrrrr
  // Where us-east-1_aaaaaaaaa is the User Pool id
  // And qqqqqqqq-1111-2222-3333-rrrrrrrrrrrr is the User Pool User Id
  const parts = authProvider.split(':');
  const userPoolIdParts = parts[parts.length - 3].split('/');

  const userPoolId = userPoolIdParts[userPoolIdParts.length - 1];
  const userPoolUserId = parts[parts.length - 1];
}
Can-Sahin commented 5 years ago

It literally costed me half a day to realize it isn't doable.

I cannot express how misleading ${cognito-identity.amazonaws.com:sub} is. Took me half a day to find out it isn't Cognito Pool's user sub, since it is the last thing I debugged.

Bridge between User Pool user attributes is a must I'd say.

Sad to see that the issue is from 2 years ago!!!

terukizm commented 5 years ago

I hope this sample will your help. (using lambda-proxy integration and cognito userpool authorizer in serverless framework) https://github.com/terukizm/sls-lambda-proxy-userpool-auth-sample

baharev commented 5 years ago

@terukizm We know that it can be solved, but we think it should be solvable with the SDK only, that is, without any extra services, frameworks, and hoops to jump. Your solution underlines the problem: You needed an extra framework (serverless) and a lambda-proxy integration. It is not simple.

Shadaeiou commented 5 years ago

@baharev I'm chiming for another user who wants this functionality. Right now I'm using Cognito to manage all my users in my app, lambda to communicate with a dynamodb, and a JS front end using amplify accessing lambda without API gateway.

In Lambda I have access to context.identity.cognitoIdentityId and nothing else. I need access to a user's attributes in the user pool via cognitoIdentityId. Or vice versa being able to take a username in the user pool and access the cognitoIdentityId.

baharev commented 5 years ago

@Shadaeiou OK. And how can I help you? This is the feature that I have been asking for; I don't have an easy way of doing it either. I think you should address one of the developers at AWS to do something about it.

Shadaeiou commented 5 years ago

@baharev I wasn't asking for help I was just saying I'm also in the same boat and desire this feature.

baharev commented 5 years ago

@Shadaeiou Then please tell that to one of the AWS developers on this thread. This issue is the oldest open issue and also the most commented one. I don't understand why it has such low priority.

keithdmoore commented 5 years ago

We all do. It’s shouldn’t be this hard. Not this hard on Google or Azure and bet. Or Oracle even

Pecked out from my iPhone

On Jul 27, 2019, at 7:43 PM, Shadaeiou notifications@github.com wrote:

@baharev I wasn't asking for help I was just saying I'm also in the same boat and desire this feature.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

babus commented 5 years ago

ah @baharev, you deserve a medal! Like @Can-Sahin said, it took me a day to realize that it still isn't possible and sad to see this has been a point of discussion for the last 2 years comparing how AWS is promoting Amplify as an inhouse serverless stack out there. Has this been raised in amplify-cli repo? I just have hit with the same issue but on a different context. Giving up on this one for now.

baharev commented 5 years ago

@babu-upcomer As for amplify-cli, the first red flag was when its predecessor (awsmobile-cli) silently and irrecoverably deleted the S3 bucket with the users' files. More red flags followed, then I ultimately stopped using amplify-cli when it required full admin access to my AWS account, and I refuse to use it. Due to the bugs I encountered, I simply don't trust it. Just look at the issues I raised in the awsmobile-cli, amplify-cli and amplify-js repos.

I have rewritten everything from Amplify on the top of the AWS JavaScript SDK that I need except Auth. The reason for not rewriting Auth is that it is non-trivial and I don't have the time at the moment to do it. But it is coming...

By the way, it is fairly easy to rewrite, for example, Storage (S3) on the top of the AWS JavaScript SDK: 15 lines of code, and then you have full control. Accessing DynamoDB is equally easy with the AWS JavaScript SDK.

I rolled my own deployment script, less than 200 lines and it does pretty sophisticated things (for example compression and cache control for CloudFront the way I want it). Now I control how Storage works, or how deployment is done. Again, look at the issues I raised in the awsmobile-cli, amplify-cli and amplify-js repos.

As for this particular issue #54, I am working towards an approach similar to what @lestephane sketched in his comment. It turns out that after restructuring my application, a similar approach works for me too. It is admittedly a workaround, but at least I will have full control. I will still depend on Auth though...

baharev commented 5 years ago

@babu-upcomer As a side note: Even if this issue is resolved, you would want to assign your unique ID to each user in a way you control. From the comment of @RossWilliams:

I’d much rather have a choice of claim to use and I’d like to avoid sub as it’s not under my control and cannot be recreated when restoring a Cognito User Pool. Anything that relies on a sub would be a large operations risk and require a lot of tooling and time to overcome.

in the related issue Can we use the sub instead of identity id for private/protected storage?

Vingtoft commented 5 years ago

I cannot believe this problem has not been fixed yet. People are burning so much time!!!!

vahdet commented 4 years ago

So, I also jump on the bandwagon. Getting another user's protected file through AWS Amplify requires the identityId such as:

Storage.get('test.txt', { 
    level: 'protected', 
    identityId: 'xxxxxxx' // the identityId of that user
})
.then(result => console.log(result))
.catch(err => console.log(err));

So, I should get that identityId not for the current user, but also for other ones!

davidolmo commented 4 years ago

Lol, I was not expecting an issue this old to keep open. I'm facing exactly the same problem. I come from Google firebase and this was super easy to accomplish with the SDK. +1 to please solve this

djkmiles commented 4 years ago

This took us about 8H to solve, the tidiest secure solution we currently have for our closed AppSync + Cognito UP + Cognito IP / AWS system is running a Lambda as a mutation that takes no parameters. The user's exact current JWT is put into the Lambda request by the AppSync VTL resolver along with the CUP ID and then the Lambda requests the Cognito IP user-identity using the CognitoIdentity module and then writes it into the "Users" DDB table using the CUP ID so it can be looked up when needed.

AppSync Lambda-request:

{
    "version" : "2017-02-28",
    "operation": "Invoke",
    "payload": $util.toJson({
        "sub": $ctx.identity.sub,
        "jwt": $ctx.request.headers.authorization
    })
}

Lambda:

const AWS = require('aws-sdk')
const cip = new AWS.CognitoIdentity()
const ddb = new AWS.DynamoDB.DocumentClient()

exports.handler = async ({ sub, jwt }, context, callback) => {
    let { IdentityId: id } = await cip.getId({ IdentityPoolId: process.env.IDENTITY_POOL_ID, Logins: { [process.env.USER_POOL_ID]: jwt } }).promise()
    await ddb.update({ TableName: 'Users', Key: { id: sub }, UpdateExpression: 'set identityToken = :id', ExpressionAttributeValues: { ':id': id } }).promise()
    callback(null, {})
}
kevin-mitchell commented 4 years ago

Hi all, I'm going to be honest and say that despite a fair amount of reading I'm not entirely sure if the question that I have is related to this ticket or not. I'm just getting into Amplify really, and Cognito, so it's a bit confusing to me at the moment.

My specific use case (I'm hoping somebody can tell me if I'm just crazy) is that I am using PubSub with Amplify, and when a user registers I need to setup some permissions - basically I need to do the equivalent of aws iot attach-principal-policy --policy-name 'myIoTPolicy' --principal '<YOUR_COGNITO_IDENTITY_ID>' but I'd like to automate this on user signup.

This would require that in the PostAuthentication_Authentication trigger I am able to get the users Cognito Identity ID... but from what I can tell, that's not possible?

It seems like perhaps the reason this isn't possible should be more obvious, but there seems like there should be SOME relationship between the user pool user and the federated cognitio identity? But from what I can tell, within these hooks I don't have access.

I'm guessing the fact that this is so confusing to me shows there is a fundamental (and significant!) gap in my understanding of how these systems should be used.

My plan for now, to work around this, is to use a different trigger (an API call) where I know I have access to the Cognito Identity ID. I guess this is likely because when you are making API calls, you're doing so as a federated user, not as a user from a "user pool"?

baharev commented 4 years ago

@kevin-mitchell If I interpret your post properly: Yes, problem well spotted, and yes, apparently that's the best workaround for the time being. Welcome to the club!

svenmilewski commented 4 years ago

My specific use case (I'm hoping somebody can tell me if I'm just crazy) is that I am using PubSub with Amplify, and when a user registers I need to setup some permissions - basically I need to do the equivalent of aws iot attach-principal-policy --policy-name 'myIoTPolicy' --principal '<YOUR_COGNITO_IDENTITY_ID>' but I'd like to automate this on user signup.

This would require that in the PostAuthentication_Authentication trigger I am able to get the users Cognito Identity ID... but from what I can tell, that's not possible?

My plan for now, to work around this, is to use a different trigger (an API call) where I know I have access to the Cognito Identity ID. I guess this is likely because when you are making API calls, you're doing so as a federated user, not as a user from a "user pool"?

This is exactly my use case. We're using an AppSync mutation for that right now to trigger the policy attachment. But, yeah, this is definetily not the clean way...

mfogel commented 4 years ago

What we are doing to solve this problem, which has worked ok so far, is we set the user pool Username to match the IdentityId from the identity pool when creating a new user. Since they match, the bidirectional mapping between the two services is now pretty trivial ;)

The new user signup flow goes like this:

We implemented this first with the lower-level SDK's linked to above. I didn't handle the mapping of this up to amplify but I understand that was a non-trivial task.

And obviously this won't work so well if you already have a bunch of users, but if you're building a new app like we are, hopefully this works for you too.

WGriffing commented 4 years ago

I am going to add in another possible solution here, but it may be of limited value and may have other drawbacks. I will not profess to be any kind of AWS or Amplify expert so if you see an issue with using this approach I would be happy to hear from you.

I found that it is possible to use a lambda authorizer (of type TOKEN, other types may be possible, but I did not test). We were already utilizing a lambda authorizer to add additional context information into our APIs so this seems like a good fit. It's still early days so I'm not sure if we'll hit any rate limits or other problems. Consider this to be an untested/alpha solution.

  1. Utilize the JWT, along with some environment variables for things like pool ID to call boto3's get_id method from the cognito-identity client.
  2. The response of that method will contain IdentityId. Use this IdentityId to call attach_principal_policy from boto3's iot client.

Example snippet from my API's lambda authorizer:

import os
import boto3

CLIENT_COGNITO = boto3.client('cognito-identity')
CLIENT_IOT = boto3.client('iot')
IDENTITY_POOL_ID = os.environ.get('IDENTITY_POOL_ID')
IOT_TOPIC_POLICY = os.environ.get('IOT_TOPIC_POLICY')
REGION = os.environ.get('REGION', 'us-east-1')
USER_POOL_ID = os.environ.get('USER_POOL_ID')

def setup_iot(token, context):
    account_id = context.invoked_function_arn.split(":")[4]
    logins = {}
    logins['cognito-idp.{}.amazonaws.com/{}'.format(
        REGION, USER_POOL_ID)] = token

    id_response = CLIENT_COGNITO.get_id(
        AccountId=account_id,
        IdentityPoolId=IDENTITY_POOL_ID,
        Logins=logins
    )

    identity_id = id_response['IdentityId']
    CLIENT_IOT.attach_principal_policy(
        policyName=IOT_TOPIC_POLICY,
        principal=identity_id)

def lambda_handler(event, context):
    token = event['authorizationToken']
    claims = verify(token) # not shown, but responsible for verifying the JWT
    principal_id = claims.get('cognito:username', None)

    if not principal_id:
        raise Exception('Unathorized')

    setup_iot(token, context)
    <other code removed>
chintanvyas360 commented 4 years ago

Surprised to see that this issue is open since Dec 2017. Neither has this been fixed nor documented to help customers. Based on a cursory read and the no. of hacks listed on this issue, it seems obvious that it should not be left to customers and should be a P0 priority in terms of features.

Ours is a SaaS application using AWS Amplify and we need a link between the Cognito Identity ID and the username for the below-mentioned use cases:

  1. Customer Support - "My name is this, I tried to upload this x file but it failed, could you please check?" There's no way we can map the S3 file path for that Cognito Identity ID to the username.
  2. Content Moderation - A user uploads some content which shouldn't be on the platform, we need to automatically email the user regarding this but can't because although we have the identity ID, we don't have associated user attributes.
  3. Storage Limit and other checks - we need to monitor application usage and trigger alerts if customers are exceeding storage tier thresholds. Same problem!

Not sure which team is taking it up (Cognito or Amplify) but it seems that this is a major issue for a lot of customers. Sorry if this sounds rude but it's a valid critical concern and we haven't been able to get around this.

keithdmoore commented 4 years ago

Due to this issue, I will never use AWS Cognito again. Likewise, I will not be using or recommending Amplify as well due to this issue being completely disregarded.

rdsedmundo commented 4 years ago

I think we should make a template and send a support inquiry to AWS with the same content pointing to this issue, demanding an answer. We're paid customers, and it's been 2 years and counting that we had no conclusive response other than someone coming and adding a few labels to the issue saying that it's on a backlog.