External id is removed from KeycloakId key while login using Google IdP

I have a Keycloak auth server running in a standalone mode. My requirement is that users should be able to log in with their Google accounts, therefore I added Google IdP following steps in the Keycloak documentation. Once a new user has successfully logged in with their Google account, the new account should be created and stored in the Postgresql DB. To achieve this I created custom user storage provider following this example. The sample covers only the fetch user details part. To support adding new users, I implement addUser method of org.keycloak.storage.user.UserRegistrationProvider interface:

@Override
public UserModel addUser(RealmModel realmModel, String s) {
    logger.info("create user with username: " + s);
    UserEntity userEntity = new UserEntity(s, null, s, s);
    em.persist(userEntity);
    em.flush();
    return new UserAdapter(kcSession, realmModel, model, userEntity);
}

While testing the flow, the Keycloak throws an exception while executing getUserById custom provider method. According to the logs this method is called multiple times. The method looks like this:

@Override 
public UserModel getUserById(String id, RealmModel realm) { logger.info("getUserById: " + id); String persistenceId = StorageId.externalId(id); System.out.println("!!! :" + StorageId.keycloakId(model, id));
    System.out.println("!!! " + persistenceId);
    Query query = em.createNativeQuery(UserStoreQueries.GET_USER_BY_ID);
    query.setParameter(1, Long.valueOf(persistenceId));

    Object[] result = (Object[]) query.getSingleResult();

    if (result == null) {
        logger.info("Could not find user by id: " + id);
        return null;
    }
    return new UserAdapter(kcSession, realm, model, prepareUserEntity(result));
}

Before fetching user from DB, the method tries to extract the user Id (which user table primary key) from the composed Keycloak ID key and use it to get user details. The thing is that at the last method call, the extracted ID value is become equal to NULL somehow, however in the previous method calls it wasn’t, see below:

14:55:05,276 INFO  [com.redhat.custom.storage.user.CustomUserStorageProvider] (default task-1) getUserByEmail: kris@gmail.com
14:55:05,328 INFO  [com.redhat.custom.storage.user.CustomUserStorageProvider] (default task-1) Could not find user by email: kris@gmail.com
14:55:05,336 INFO  [com.redhat.custom.storage.user.CustomUserStorageProvider] (default task-1) getUserByUsername: kris@gmail.com
14:55:05,342 INFO  [com.redhat.custom.storage.user.CustomUserStorageProvider] (default task-1) Could not find user by username: kris@gmail.com
14:55:05,343 INFO  [com.redhat.custom.storage.user.CustomUserStorageProvider] (default task-1) create user with username: kris@gmail.com
14:55:05,519 INFO  [com.redhat.custom.storage.user.CustomUserStorageProvider] (default task-1) getUserById: f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:9
14:55:05,519 INFO  [stdout] (default task-1) !!! :f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:9
14:55:05,520 INFO  [stdout] (default task-1) !!! 9
14:55:05,563 INFO  [com.redhat.custom.storage.user.CustomUserStorageProvider] (default task-1) getUserById: f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:9
14:55:05,563 INFO  [stdout] (default task-1) !!! :f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:9
14:55:05,564 INFO  [stdout] (default task-1) !!! 9
14:55:05,718 INFO  [com.redhat.custom.storage.user.CustomUserStorageProvider] (default task-1) getUserById: f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:null
14:55:05,719 INFO  [stdout] (default task-1) !!! :f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:null
14:55:05,719 INFO  [stdout] (default task-1) !!! null
14:55:05,720 ERROR [org.jboss.as.ejb3.invocation] (default task-1) WFLYEJB0034: Jakarta Enterprise Beans Invocation failed on component CustomUserStorageProvider for method public default org.keycloak.models.UserModel org.keycloak.storage.user.UserLookupProvider.getUserById(org.keycloak.models.RealmModel,java.lang.String): javax.ejb.EJBTransactionRolledbackException: For input string: "null"
    at org.jboss.as.ejb3@23.0.2.Final//org.jboss.as.ejb3.tx.CMTTxInterceptor.invokeInCallerTx(CMTTxInterceptor.java:219)
    at org.jboss.as.ejb3@23.0.2.Final//org.jboss.as.ejb3.tx.CMTTxInterceptor.required(CMTTxInterceptor.java:392)
    at org.jboss.as.ejb3@23.0.2.Final//org.jboss.as.ejb3.tx.CMTTxInterceptor.processInvocation(CMTTxInterceptor.java:160)
    at org.jboss.invocation@1.6.0.Final//org.jboss.invocation.InterceptorContext.proceed(InterceptorContext.java:422)
    at org.jboss.invocation@1.6.0.Final//org.jboss.invocation.InterceptorContext$Invocation.proceed(InterceptorContext.java:509)
    at org.jboss.weld.core@3.1.6.Final//org.jboss.weld.module.ejb.AbstractEJBRequestScopeActivationInterceptor.aroundInvoke(AbstractEJBRequestScopeActivationInterceptor.java:81)

I am stuck with this. Appreciate, if someone can explain me why my user table PK ID value is removed from the Keycloak composite key?

Hello, I am not sure I understand you correctly. At the first sight it seems you are mixing two things together identity brokering and user federation.

Identity brokering provides login using and external idp e.g. google in your case. When logging in for the first time a new user is created in Keycloak’s database. Then it is possible to just click Google on the login page and user should be automatically logged in.

User federation is used, when you want to use some external storage of users instead of Keycloak database (or alongside Keycloak database). External storage can be for example LDAP, another database, local file e.t.c.

If I understand you correctly, you don’t to implement anything yourself. The workflow you described should work out of the box after creating Google idp in the admin console and configuring Postgresql as the database for Keycloak.

1 Like

@mhajas, thank you for your answer. Correct, IdP brokering and User Federation are different things. Sorry, I was unclear in my message.

Requirements, I am trying to implement:

  1. User details should be hold in the external storage - Postresql DB. Therefore, I have to use User federation. To connect Keycloak to this external storage, I implemented custom User storage SPI and added it as a provider on the Keycloak User Federation page. I tested configuration by inserting users into DB and trying to login with their valid credentials.
  2. The second requirement is that users should be able to log in with their social network account like Google. I followed this instruction to add Google Social IdP. That works as well.
  3. The last but not least requirement is to create new user account (insert record into the external storage) for the new users that successfully logged in with their Google account. To achieve that I extended by custom User SPI by implementing UserRegistrationProvider.addUser(RealmModel realm, String username) method. During testing the this flow (new user login via Google) I am getting exception I provided in the first post. The exception itself is trivial - external ID is null. But I don’t understand why it is actually null, as according to the logs of the previous getUserById method calls it is not f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:f:1b171a2b-0d7f-42eb-9c93-89fd7c71347b:9: . As a result user auth fails eventually.
    WFLYEJB0034: Jakarta Enterprise Beans Invocation failed on component CustomUserStorageProvider for method public default org.keycloak.models.UserModel org.keycloak.storage.user.UserLookupProvider.getUserById(org.keycloak.models.RealmModel,java.lang.String): javax.ejb.EJBTransactionRolledbackException: For input string: "null" .

P.S. I am new to Keycloak but I am very eager to lear it and use in a proper way. Hope you can help me to solve the issue.

Thank you for more details. Now I understand. The steps you have done seem correct to me and I don’t see any reason why it should fail this way. The null value seems to be coming from the Keycloak codebase. This might be a bug. Could you please create a new issue in our issue tracker? Or, if you are interested you can try to debug where the null value is coming from. I believe the last line before your provider is called is here, so you can try to put a breakpoint there and look from where it is called.