Social Login not working as expected with User Storage SPI

Hi everyone,

I am working on creating a User Storage SPI for my Keycloak setup with the aim of migrating users from my external users table (created using legacy auth) into Keycloak. Here’s the flow I am aiming for:

  1. A user attempts to log in to my platform via Google.
  2. Keycloak first searches its internal database for the user.
  • If the user is found, it returns the user.
  • If the user is not found, it searches the external database.
    • If the user is found in the external database, it adds the user to Keycloak’s internal database and then returns the user.
    • If the user is not found in the external database, it creates a new user in both Keycloak’s internal database and the external database. (I need to maintain the external users table, which is why new users are also added there.)

From my understanding of the SPI, whenever a social login occurs, getUserByEmail is triggered. I have added logic in this method to search for the user in Keycloak’s internal database and the external database, and to create the user if necessary. Once getUserByEmail returns a Keycloak user object, I would expect the flow to be complete.

However, the issue I’m facing is that when a user already exists in the external database, after getUserByEmail returns a valid UserModel (i.e., the user is found in the external database and created in Keycloak’s internal database), Keycloak seems to be creating the social user again. This results in a duplicate account screen and a prompt to link the accounts.

Here’s my getUserByEmail code

        // Keycloak calls this function only after it's not able to find the user in it's internal DB
        // Searching for the user in external DB
        CriteriaQuery<ExternalUserModel> query = createCriteriaQuery(AppConstants.EMAIL, email);
        try {
            ExternalUserModel user = null;
            try {
                user = externalDbSession.createQuery(query).getSingleResult();
            } catch (NoResultException nre) {
                // User not found in external DB, returning null so that Keycloak can invoke addUser which will take care of user creation in both Keycloak and External DB.
                return null;
            }
           
           // create user in Keycloak
           UserModel user = UserStoragePrivateUtil.userLocalStorage(session).addUser(realm, externalUser.getId(), transformedUsername, true, false);
            user.setFederationLink(model.getId());
            FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel("google", externalUser.getGoogleId(), externalUser.getEmail());
            session.users().addFederatedIdentity(realm, user, federatedIdentityModel);
            user.setEmail(externalUser.getEmail());
            user.setEnabled(true);
            return user;
        } catch (SQLException e) {
            return null;
        }

Any help will be appreciated. Am I missing something, or have I misunderstood something about the user storage SPI ?

Do you mean it occurs on subsequent login (not the 1st)?

Nope, it occurs on the first login itself.

  1. A user who is a social user in my external provider tries logging in via social login on Keycloak.
  2. The User Storage SPI is triggered, and getUserByEmail is invoked to handle the user migration from the external storage provider to Keycloak.
  3. getUserByEmail returns a Keycloak user object, expecting to complete the login flow.
  4. However, Keycloak still calls addUser, and right before it does, it checks if the user already exists. Since the user was already created in getUserByEmail, this results in an account linking screen being displayed.

A normal user flow should remain seamless. If a user previously logged in via Social on my application, they should continue to do so after migrating to Keycloak for authentication. However, when they attempt to log in via Social with Keycloak, they encounter an account linking screen, disrupting their usual login experience.

see also my answer here: How can I distinguish between a Social Login and an Email Login in User Storage SPI? - #4 by dasniko