Wrong id token when logging in user with 3rd party SAML integrated with Cognito linked with AdminLinkProviderForUser


Our users have a regular username-password Cognito user in a user pool and we want to allow them to alternatively log in using their SAML Idp. But, we need the username in their native username-password user in order to grant them appropriate access. So, we use the AdminLinkProviderForUser feature to link the new user to the existing user in a pre-sign up lambda function. As a note, we are using the authorization code flow, so we also call the token endpoint after receiving back a code.

We use boto3 to send the link request and that looks like this (with <tokens> showing the meaning of the variables): client.admin_link_provider_for_user(UserPoolId=user_pool_id, DestinationUser={'ProviderName': 'Cognito', 'ProviderAttributeValue': <the username of the existing native cognito user>}, SourceUser={'ProviderName': <the name we gave to the idp in cognito>, 'ProviderAttributeName': 'Cognito_Subject', 'ProviderAttributeValue': <the default value generated from the SAML assertion by cognito and provided in the event>})

This seems to making a couple of changes:

  1. An identity is added to the existing, native cognito user:
    [{"userId":<the default value generated from the SAML assertion by cognito and provided in the event>,"providerName":<the name we gave to the idp in cognito>,"providerType":"SAML","issuer":null,"primary":false,"dateCreated":1661118033932}]

  2. And a new user is created that has the generated Cognito_Subject as its username and that has its own linked identity: [{"userId":<the NameID from the SAML assertion>,"providerName":<the name we gave to the idp in cognito>,"providerType":"SAML","issuer":<the urn for the issuer of the assertion>,"primary":true,"dateCreated":1661118034135}]

So, it appears that the existing, native cognito user points to the newly created, external provider user, which, in turn, has an identity that is the external NameID. I think this appears right, at least conceptually, however, I'm not sure about why the external idp is designed as primary. But, anyway, this configuration does allow the user to log in and does end with an authorization code being returned. And, sending the authorization code to the token endpoint does result in id and access tokens being returned.

However, when I decode the id token, it turns out that it is for the newly created user that has the Cognito_Subject value as its username (the source user) instead of the existing, native user that was specified as the destination user. I also tried linking them using the email attribute from the SAML assertion, and while the change appeared to work, in that the new user was created with email address as its username, it still resulted in the source user being used to create the id token instead of the destination user.

So, a couple of things I've been thinking about:

  • Based upon the doc, I don't think I've got the users mixed up. I'm using the existing, cognito native user as the destination when creating the link and the new user, with the external SAML NameID identifier, as the source user. That seems right to me based upon the doc.
  • The linked identities do seem to be created, but the pointers seem to be like this: native user in cognito -> external provider user in cognito -> external NameID.
  • But, if that is the correct way to link them, then how does Cognito know to follow the links back to the original native user and return it instead? I mean, there is certainly a way to do that, but it seems it would be less efficient than if the links went the other direction, so it makes me question whether I'm misunderstanding the doc.

To sum up, am I correct to expect the id token to represent the destination user? Is it correct to link the users in the direction I described? And, if the answer to both of those questions is yes, then does anyone have any idea why the links might not be working, given what I described we are doing?

Thanks in advance to anyone who can help me with this.

asked 2 years ago580 views
4 Answers

I have exactly the same behavior for custom OIDC provider: original Cognito user gets identity added to the list of identities, but along that one more external user is added alongside to users. It's identity differs only by date created and primary set to true. I am also able to get access and id token, but they are for separate identity (the "second" user).

Interestingly enough, with second provider - Google- behavior is as expected: after the same pre-sign up lambda executes AdminLinkProviderForUser command, original Cognito user gets all attributes updated, as well as list of identities expanded. When id and access tokens are examined - it is still original Cognito user.

This article puts some light on it https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminLinkProviderForUser.html

I have tried linking custom OIDC provider (LinkedIn) with id, user_id, sub instead of Custom_Subject; with full (linkedin_xyz), and partial (xyz) username - but no luck. It does not get better than behavior above.

Some piece of information is missing on how to set up linking for custom OIDC like LinkedIn.

Any advice would be appreciated. Max.

answered 4 months ago
  • My response to you was too long to be a reply to your comment, so you will find it below. Sending this just to be sure you get a notification...since my solution just might work for you.


Few things to remember

  1. When user authenticates with federated linked account, the user will be authenticated as native cognito user and generated tokens will hold identity of native account.
  2. Any mapped attributes from federated IdP to native user and will be overwritten with each login.
  3. Linking external account (source) to Destination account should be two step a. Delete the external account source account in user pool. b. Creating a link to native account using AdminLinkProviderForUser
answered 2 years ago

I posted the original question and the official answer didn't solve it for me. But I did some trial and error and figured it out by deducing based on the behaviors I was seeing from Cognito. What fixed it for me, though, may not solve your problem, but here goes.

Through debugging I found that the generated identifiers, which are composed from, among other info, the external username, were getting lowercased before being sent to the pre-signup trigger and, through token inspection, I saw that the corresponding usernames from the source tokens were mixed case (in my situation). Based on those observations, I tried changing my case-insensitive userpool to case-sensitive and it worked. I'll explain why.

What appears to be happening is that, during login, Cognito is always checking for a match in the userpool based on the exact string from the external token. But, for case-insensitive userpools, as I said, on first login, it is always transforming it to lowercase before sending it to the pre-sign trigger. So, when the source idp uses mixed-case or uppercase, it never finds a match. To be clearer, I think the sequence Cognito follows is like this for login: 1. inspect the token and check the userpool for an exact (case-sensitive) match, 2. if no exact match is found, generate a new identifier (with a lowercased value) and send it to the pre-sign function for linking. Think through that for a second and I think you will see the problem.

The reason it always works with Google turned out to be because Google always uses lowercase for the values. What I did to work around it was to make the userpools case-sensitive, so that the pre-sign trigger would receive values with the correct case. This, of course, meant that I had to abstract access to cognito for the application so that users don't get created with the wrong case. One other note, you cannot change the case-sensitivity for a userpool once you create it, so if you have a userpool that is already in use, this solution would require a user migration.

I think the right solution is for Cognito to to either stop lowercasing the identifiers it generates, even on case-insensitive userpools, when it calls the triggers or for the login for case-insensitive userpools to allow any case when detecting a match. But, I didn't have the time to wait around for AWS to fix that. Good luck.

answered 4 months ago

Found the issue. When provider has letters in the userId, cognito transforms their case to lower one within userId. That's the issue as sub is case-sensitive.

Solution is to add one more mapping: custom:sub. Go to sign up experience, and then add "sub" into custom mapping. It will be added as custom:sub Then go to sign in experience > LinedIn and map sub into custom:sub there. This will allow for pre-sign up function to be as follows for linkedin provider to work:

    console.log(`Linking ${userName} (ID: ${userId}).`);

    console.log(`Linking userAttributes (ID: ${JSON.stringify(event)}).`);  

    const finalUserId = providerName === 'linkedin' ? event.request.userAttributes["custom:sub"]: userId;

    const command = new AdminLinkProviderForUserCommand ({
        // Existing user in the user pool to be linked to the external identity provider user account.
        DestinationUser: {
            ProviderAttributeValue: existingUsername,
            ProviderName: 'Cognito'
        // An external identity provider account for a user who does not currently exist yet in the user pool.
        SourceUser: {
            ProviderAttributeName: 'Cognito_Subject',
            ProviderAttributeValue: finalUserId,
            ProviderName: providerName // Facebook, Google, Login with Amazon, Sign in with Apple
        UserPoolId: userPoolId
    await  cognito.send(command);

Here is event data example :

    "version": "1",
    "region": "us-east-1",
    "userPoolId": "us-east-1_d3sdfdsfD",
    "userName": "linkedin_0labhk9l5z",
    "callerContext": {
        "awsSdkVersion": "aws-sdk-unknown-unknown",
        "clientId": "dskjflkdsjflkdsflkdshkfdshlfk"
    "triggerSource": "PreSignUp_ExternalProvider",
    "request": {
        "userAttributes": {
            "email_verified": "false",
            "cognito:email_alias": "",
            "cognito:phone_number_alias": "",
            "custom:sub": "0lAbhK9l5z",
            "given_name": "Tom",
            "family_name": "Jones",
            "email": "tom@example.com"
        "validationData": {}
    "response": {
        "autoConfirmUser": false,
        "autoVerifyEmail": false,
        "autoVerifyPhone": false

What I read online is - facebook and amazon have case-sensitive letters in sub as well. In contrast Google has numbers, and hence does not need this hack.

Hope this helps.


answered 3 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