Cognito auth flow fails with "Already found an entry for username Facebook_10155611263153532"
Asked Answered
A

9

58

The goal is to implement a social provider auth flow as described in User Pools App Integration and Federation.

One important thing that I want to satisfy, is to merge user pool accounts that have the same email address.

I am accomplishing that by calling adminLinkProviderForUser within the PreSignUp_ExternalProvider cognito lambda trigger.

So with this, everything works. The new social provided user is being registered and linked with the already existing Cognito (user+pass) user.

However, the authentication flow, from user's perspective doesn't complete. It fails at the last step where the callback uri (defined in cognito user pool) is being called:

error: invalid_request

error_description: Already found an entry for username Facebook_10155611263152353

But then, if the user retries the social auth flow, everything works, and would get session tokens that represent the original Cognito User Pool user (the one that already had that email).

Note that I'm testing the auth flow on an empty User Pool, zero user accounts.

Alysiaalyson answered 14/12, 2017 at 14:0 Comment(1)
Added your questions to AWS REPOST: repost.aws/questions/QUgWVkIodQS1W3Yj8MYjInbA/…Swivel
E
39

For all the poor souls fighting with this issue still in 2020 the same way I did:

  • I have eventually fixed the issue by catching the "Already found an entry for username" in my client application and repeating the entire auth flow once more.
  • Luckily the error only gets fired on the initial external provider signup but not in the subsequent signins of the same user (cause it happens during signup trigger, duh). I'm taking a wild guess, but here is what I think is happening:
    • In my case, the facebook provider was getting succesfully linked with the pre-existing cognito email/password user. new Facebook userpool entry linking to the email/password user was succesfully created.
    • Still, it seems like cognito tried to register the fully isolated Facebook_id user during the internal signup process (even though a link user entry with the same username was already created in the previous step). Since the "link user" with the username Facebook_id was already existing, cognito threw an "Already found an entry for username Facebook_id error" internal error.
    • This error has been repeatedly voiced over to the AWS developers since 2017 and there are even some responses of them working on it, but in 2020, it's still not fixed.
Egret answered 13/3, 2020 at 15:32 Comment(18)
Could you share more information on how you repeat the entire auth flow once more? Are you using Amplify on frontend? I tried to repeat the entire auth flow but it keeps redirecting and append code=random_string&state=random_string to my cognito preset redirect url.Henslowe
I am using amplify. I am guessing you're using some form of authentication to get your users to sign up /sign in into your application. By repeating the entire auth flow, I mean repeating the process of calling the social provider sign up/sign in service once again after your user has already signed up into your application, so your flow would look like this: 1. your user signs up throught facebook since he hasnt signed up into your app before 2. you get the error during signup 3. you catch the error in the client by parsing the callback url 4. you call the sign in once moreEgret
How are you doing the linking described in "facebook provider was getting succesfully linked with the pre-existing cognito email/password user"?Hysterical
@Hysterical there are two use cases to consider: 1. the user was already registered via email/password 2. the user uses social provider for his first registration I first look whether the given user already exists using the AWS.CognitoIdentityServiceProvider() listUsers method if he does I use AWS.CognitoIdentityServiceProvider() adminLinkProviderForUser method to link them togetherEgret
@Egret how do you deal with the user having to authenticate twice now with the social provider? I am having the same issue and workaround but my users are seeing the SignInWithApple popup twice. Kinda bad UX.Cabinet
@Tristan I am currently having the same issue with apple sign in and this is the current setup I am running to keep my apps in the apple store. I am planning to start experimenting with using Post authentication cognito trigger to link the email and social provider row together, but I haven't gotten to it yet. The hypothesis is that if you are connecting pre-existing cognito user rows (one for your email registration and one for your federation), it could link them together without having to create a new row. That would fix the "already found an entry for username" error alltogether.Egret
Having the same issue as well with SAML. I wanted to link between an already existing user to a SAML one. Luckily it's not a big deal for me not to link them but for sure it's unexpected.Brandibrandice
@Egret any luck with "the hypothesis" on that post auth trigger?Reflexion
@Egret Calling Auth.federatedSignIn({provider: 'Google'}) twice works for Google but not for SignInWithApple. I have tried Post authentication cognito trigger to link the email and social provider row together before, it didn't work for me as link can only be done before the EXTERNAL_PROVIDER account is created. Have you found other way?Henslowe
@Jun711 I am facing the same issues with every other provider except Google. Is there someone with a AWS Support contract (I don't have any, yet) who is willing to poke AWS again at this issue?Cabinet
@Cabinet If you use Amplify, you can log an issue on their GitHub github.com/aws-amplify/amplify-jsHenslowe
@Jun711 I don't use Amplify unfortunately.Cabinet
@Cabinet You can look up similar issue on AWS discussion forum or open an issue if there isn't one forums.aws.amazon.com/forum.jspa?forumID=173Henslowe
Faced the same issue not even sure if cognito is worth using. This service has wasted more time than it has saved. Disappointed. Truly disappointed.Knowledgeable
Isn't there any reasonable way to fix this yet?Conversational
@MostafaShahverdy I abandoned Cognito, the service is till date really disappointing.Cabinet
After many trials and pains with Cognito, I would also highly recommend using another service, there are many things that are unfinished, buggy or not thought through. Unfortunately I have to say that these days I consider AWS Cognito a vendor-locking honeypot and am strictly avoiding it.Egret
Guys, please raise a ticket in your AWS accounts to raise awareness of this bug even more. And upvote this question: repost.aws/questions/…Swivel
H
5

Yes, this is how it is currently setup. If you try to link users using PreSignUp trigger, the first time won't work. A better way to handle this(I think) would be to provide an option in your UI to link external accounts on sign-in. In the pre-signup trigger, search for a user with the same unique attribute (say email) and see if the sign up is from external provider. Then show a message such as email already exists. Login in & use this menu/option to link. Haven't tested this though.

Heighten answered 18/12, 2017 at 10:48 Comment(1)
That's a poor UX. If a user logs in with a different provider (maybe they forgot which one they originally used), it should auto-link transparently.Hysterical
B
4

Did this bug all of a sudden stop happening on 2/21/23? We didn't change anything but now this is no longer happening to users on their first time signing up. We also noticed that the UI for how Cognito is showing linked users is different - there is just 1 cognito account you're able to see in Cognito instead of multiple. You can still see the federated linked accounts in the identities property though

Billowy answered 22/2, 2023 at 12:45 Comment(0)
E
3

I finally got this thing working in a non-weird way where users have to authorize twice or other things.

Process explained:

  1. User tries to authenticate using an identity provider, for the first time => PreSignUp lambda kicks in and check if user exists via email

1a. If the user exists, it will throw an error, eg. CONFIRM_IDENTITY_LINK_token that I'm capturing on the client. token is a base64 string with the username and identity id ("username:facebook_123456")

1b. If the username does not exist, I create a new user with a temporary password and throw an error FORCE_CHANGE_PASSWORD_token. Same token but I add the temporary password to this time.

  1. In the client I have one callback route '/authorize' => this is the one you set up as a callback URL in Cognito, and 2 extra routes: '/confirm-password' and '/configure-password'.

In the /authorize route I'm capturing the errors and getting the attached tokens and redirect to the extra routes: 1a => /configure-password?token=token and 1b => /confirm-password?token=token

For "/confirm-password" I ask the user to confirm its current password in order to authorize linking with the provider, then use the token to log him in with the identity id as clientMetadata, eg "{"LINK_PROVIDER": "facebbok_12345678"}"

On login, I have a PostAuthentication lambda which checks for the "LINK_PROVIDER" in the clientMetadata, and links it to the user.

For "/configure-password" I parse the token and do a "shallow" login with the credentials from the token and identity id as client metadata (same as above) then prompt the user to configure a new password for his account.

I know it might seem a little bit restrictive but I find it better than to authorize twice.

Also, this does not create extra users for identities in the user pool.

Code examples:

PreSignUp lambda

export async function handler(event: PreSignUpTriggerEvent) {
  try {
    const { userPoolId, triggerSource, request, userName } = event
    if (triggerSource === 'PreSignUp_ExternalProvider') {
      // Check if user exists in cognito
      let currentUser = await getUserByEmail(userPoolId, request.userAttributes.email)

      if (currentUser) {
        // User exists, thow error with identity id
        const identity = Buffer.from(`${currentUser}:${userName}`).toString('base64')
        throw new Error(`CONFIRM_USER_IDENTITY_${identity}`)
      }

      // Create new Cognito user with temp password
      const tempPassword = generatePassword()
      currentUser = await createNewUser(userPoolId, request.userAttributes, tempPassword)
      // Throw error with token
      const state = Buffer.from(`${currentUser}:${tempPassword}:${userName}`).toString('base64')
      throw new Error(`FORCE_CHANGE_PASSWORD_${state}`)
    }
    return event
  } catch (error) {
    throw new Error(error)
  }
}

PostAuthentication lambda

export async function handler(event: PostAuthenticationTriggerEvent) {
  try {
    const { userPoolId, request, userName } = event

    if (request.clientMetadata?.LINK_IDENTITY) {
      const identity = request.clientMetadata['LINK_IDENTITY']
      // Link identity to user
      await linkIdentityProvider(userPoolId, userName, identity)
    }

    return event
  } catch (error) {
    console.error(error)
    throw new Error('Internal server error')
  }
}
Eustasius answered 30/10, 2021 at 11:46 Comment(0)
V
1

To elaborate on @agent420's answer, this is what I am currently using (Typescript example).

When a social identity attempts to sign up and the email address already exists I catch this using the PreSignUp trigger and then return an error message to the user. Inside the app, on the user's profile page, there is an option to link an identity provider which calls the adminLinkProviderForUser API.

import {
    Context,
    CognitoUserPoolTriggerEvent,
    CognitoUserPoolTriggerHandler,
} from 'aws-lambda';
import * as aws from 'aws-sdk';
import { noTryAsync } from 'no-try';

export const handle: CognitoUserPoolTriggerHandler = async (
    event: CognitoUserPoolTriggerEvent,
    context: Context,
    callback: (err, event: CognitoUserPoolTriggerEvent) => void,
): Promise<any> => {
    context.callbackWaitsForEmptyEventLoop = false;

    const { email } = event.request.userAttributes;

    // pre sign up with external provider
    if (event.triggerSource === 'PreSignUp_ExternalProvider') {
        // check if a user with the email address already exists

        const sp = new aws.CognitoIdentityServiceProvider();

        const { error } = await noTryAsync(() =>
            sp
                .adminGetUser({
                    UserPoolId: 'your-user-pool-id',
                    Username: email,
                })
                .promise(),
        );

        if (error && !(error instanceof aws.AWSError)) {
            throw error;
        } else if (error instanceof aws.AWSError && error.code !== 'UserNotFoundException') {
            throw error;
        }
    }

    callback(null, event);
};

Veta answered 22/12, 2019 at 11:45 Comment(0)
E
1

We faced the same issue and tried various hacks to get around it. As we started to use SignInWithApple, we couldn't handle it with the 'double turnaround' because Apple always wants the user to enter their email and password, not like Google, where the second time, everything works automatically. So the solution we ended up building was to store the Cognito/IdP ID (Google_1234, SignInWithApple_XXXX.XXX.XXX) in our database but still create a native Cognito user that isn't linked via Cognito. The native user is created to make unlinking easier because first, we get rid of the data (IdP user-id) we store in our database and then the Cognito IdP user. The user can then proceed using the Native Cognito user. Then we have a middleware component in place that allows us to have JWT in the external IdP or Cognito native format and translates so we can use both versions. As long as the user uses an IdP/SSO, we reset the Native users' password to a very long random value and prevent resetting it, so they must use the IdP.

So whatever you are trying to do, prevent using the admin-link-provider-for-user command!

Eigenfunction answered 2/6, 2022 at 13:6 Comment(0)
M
0

The same code in JavaScript getUser has been called instead of listUsers. It is also assumed that all users have their email id as their username.

const aws = require('aws-sdk');

exports.handler = async (event, context, callback) => {
    console.log("event" + JSON.stringify(event));
    const cognitoidentityserviceprovider = new aws.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});
    const emailId = event.request.userAttributes.email
    const userName = event.userName
    const userPoolId = event.userPoolId
    var params = {
        UserPoolId: userPoolId,
        Username: userName
    };
    var createUserParams = {
        UserPoolId: userPoolId,
        Username: emailId,
        UserAttributes: [
            {
                Name: "email",
                Value: emailId
            },
        ],
        TemporaryPassword: "xxxxxxxxx"
    };

    var googleUserNameSplitArr = userName.split("_");
    var adminLinkUserParams = {
        DestinationUser: {
            ProviderAttributeName: 'UserName',
            ProviderAttributeValue: emailId,
            ProviderName: "Cognito"
        },
        SourceUser: {
            ProviderAttributeName: "Cognito_Subject",
            ProviderAttributeValue: googleUserNameSplitArr[1],
            ProviderName: 'Google'
        },
        UserPoolId: userPoolId
    };

    var addUserToGroupParams = {
        GroupName: "Student",
        UserPoolId: userPoolId,
        Username: emailId
    };

    if (userName.startsWith("Google_")) {
        await cognitoidentityserviceprovider.adminGetUser(params, function (err, data) {
            if (err) {
                console.log("No user present")
                console.log(err, err.stack);
                cognitoidentityserviceprovider.adminCreateUser(createUserParams, function (err, data) {
                    if (err) console.log(err, err.stack);
                    else {
                        console.log("User Created ")
                        cognitoidentityserviceprovider.adminAddUserToGroup(addUserToGroupParams, function (err, data) {
                            if (err) console.log(err, err.stack);
                            else {

                                console.log("added user to group");
                                console.log(data);
                            }
                        });
                        cognitoidentityserviceprovider.adminLinkProviderForUser(adminLinkUserParams, function (err, data) {
                            if (err) console.log(err, err.stack);
                            else {
                                console.log("user linked");
                                console.log(data);
                            }
                        });
                        console.log(data);
                    }
                });
            } else {
                console.log("user already present")
                cognitoidentityserviceprovider.adminLinkProviderForUser(adminLinkUserParams, function (err, data) {
                    if (err) console.log(err, err.stack); // an error occurred
                    else {
                        console.log("userlinked since user already existed");
                        console.log(data);
                    }
                });
                console.log(data);
            }
        });
    }
    console.log("after the function custom");
    callback(null, event);
};
Mexicali answered 10/9, 2020 at 12:54 Comment(0)
F
0

This is a well know error. I handle it by retrying the request after this error and it will work. The error is because there is not way in the SDK to let it know to the pool that you already link the Federation Credentials to an user and it try to create a new user with those credentials

Fricative answered 30/10, 2021 at 23:54 Comment(0)
M
0

I wanted to have the feature of having a user seamlessly being able to login with one social provider (ex: Facebook) and then another one (Google). I struggled with the retry process, especially with Google Login. At the signup process, if a user have several accounts, he will need to process twice the account selection.
What I ended up doing is just using Cognito for the client side code and token generation and have a lambda in the pre signup process mapping userIds with their email in a custom DB (Postgres or DynamoDB). Then when a user query my API, based on their userId (whether it's a FacebookId or a cognito email userId, I am querying the DB to find the linked email and I am able to authenticate any users and their data like this.

Mcnair answered 15/2, 2022 at 13:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.