keycloak-user-migration
keycloak-user-migration copied to clipboard
Username with uppercase letters causing errors
Summary
TL;DR: Error migrating users to keycloak when the username is not all lowercase.
In our API, given a user with username [email protected]
(we're using the email as username).
When they're first migrated to keycloak using the provider, we got the following error in the logs
2024-03-18 10:43:10,361 WARN [org.keycloak.services] (executor-thread-1148) KC-SERVICES0013: Failed authentication: java.lang.IllegalStateException: Local and remote users differ: [[email protected] != [email protected]]
2024-03-18 10:43:10,359 INFO [com.danielfrak.code.keycloak.providers.rest.remote.UserModelFactory] (executor-thread-1148) Creating user model for: [email protected]
The user is created in keycloak, but the only info they have is the username, everything else is blank.
Steps to reproduce
- On the GET API side, return a user model with an uppercase letter on their username
{ "id": "123", "username": "[email protected]", "email": "[email protected]", "firstName": "Alice", // ... }
What you expected would happen
The user with username [email protected]
is created on keycloak with all the information provided on the json from the API (even a username [email protected]
would be accepted in this case)
What actually happens
A user is created on keycloak with the username [email protected]
with the status disabled
, no other information is migrated (email, firstname, lastname, attributes, credentials, ...)
The user is not able to login.
Notes
We've tracked it to this part of code
As a hack, we're now forcing the username to be all lowercase in the GET api, and it's working fine.
Hello,
That's a bit concerning!
From what I understand, the username still has the correct case by the time we reach UserModelFactory::create
. Therefore, it seems that the line userModel = session.users().addUser(realm, legacyUser.getUsername()); is actually the culprit which changes the username's case.
Ergo, signs point to your instance of Keycloak changing the case - are you sure you haven't configured Keycloak to force lowercase? I haven't used it in a while, so I'm not entirely sure how or where that could be done.
Are you able to reproduce this behavior via a Cypress test, using the provided dockerized Keycloak?
I'm not aware of any config we did in the team to force the case.
I'll try to reproduce using cypress.
I can confirm that the e2e tests are failing using the provided docker compose 😬
And I got the same logs as well.
I've updated the legacysystemexample
to return a username with an uppercase letter in it, here is the result.
Test before change
Code changes
Tests after change
Log trace
legacy-system-example-1 | 2024-03-19T17:55:06.670Z INFO 1 --- [nio-8080-exec-3] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:55:06,683 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:55:06.691Z INFO 1 --- [nio-8080-exec-5] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:55:06,694 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:55:06.704Z INFO 1 --- [nio-8080-exec-7] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:55:06,707 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:55:06.713Z INFO 1 --- [nio-8080-exec-8] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:55:06,715 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:55:06.721Z INFO 1 --- [nio-8080-exec-1] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:55:06,724 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:55:06.733Z INFO 1 --- [nio-8080-exec-2] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:55:06,735 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: lucy
keycloak-1 | 2024-03-19 17:55:06,760 WARN [org.keycloak.events] (executor-thread-14) type=LOGIN_ERROR, realmId=42684046-75b5-40df-a4f0-14c8126051b7, clientId=account-console, userId=null, ipAddress=192.168.65.1, error=user_not_found, auth_method=openid-connect, auth_type=code, redirect_uri=http://localhost:8024/realms/master/account/#/, code_id=4cb73fcc-d451-40c5-8d33-cf108ef9b66c, username=lucy
legacy-system-example-1 | 2024-03-19T17:55:07.969Z INFO 1 --- [nio-8080-exec-4] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:08,014 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:08.019Z INFO 1 --- [nio-8080-exec-6] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:08,024 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:08.028Z INFO 1 --- [nio-8080-exec-9] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:08,031 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:08.038Z INFO 1 --- [io-8080-exec-10] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:08,043 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:08.046Z INFO 1 --- [nio-8080-exec-2] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:08,049 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:08.052Z INFO 1 --- [nio-8080-exec-4] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:08,055 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:08.060Z INFO 1 --- [nio-8080-exec-6] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:08,062 INFO [com.danielfrak.code.keycloak.providers.rest.remote.UserModelFactory] (executor-thread-14) Creating user model for: Lucy
keycloak-1 | 2024-03-19 17:55:08,071 WARN [org.keycloak.services] (executor-thread-14) KC-SERVICES0013: Failed authentication: java.lang.IllegalStateException: Local and remote users differ: [lucy != Lucy]
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.remote.UserModelFactory.validateUsernamesEqual(UserModelFactory.java:109)
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.remote.UserModelFactory.create(UserModelFactory.java:71)
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.LegacyProvider.lambda$getUserModel$1(LegacyProvider.java:60)
keycloak-1 | at java.base/java.util.Optional.map(Optional.java:260)
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.LegacyProvider.getUserModel(LegacyProvider.java:60)
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.LegacyProvider.getUserByEmail(LegacyProvider.java:168)
keycloak-1 | at org.keycloak.storage.UserStorageManager.lambda$getUserByEmail$20(UserStorageManager.java:398)
keycloak-1 | at org.keycloak.utils.ServicesUtils.lambda$timeBoundOne$1(ServicesUtils.java:83)
keycloak-1 | at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
keycloak-1 | at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
keycloak-1 | at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
keycloak-1 | at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:400)
keycloak-1 | at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
keycloak-1 | at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
keycloak-1 | at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:528)
keycloak-1 | at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513)
keycloak-1 | at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
keycloak-1 | at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150)
keycloak-1 | at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
keycloak-1 | at java.base/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:647)
keycloak-1 | at org.keycloak.storage.UserStorageManager.getUserByEmail(UserStorageManager.java:398)
keycloak-1 | at org.keycloak.models.cache.infinispan.UserCacheSession.getUserByEmail(UserCacheSession.java:399)
keycloak-1 | at org.keycloak.authentication.authenticators.resetcred.ResetCredentialChooseUser.action(ResetCredentialChooseUser.java:101)
keycloak-1 | at org.keycloak.authentication.DefaultAuthenticationFlow.processAction(DefaultAuthenticationFlow.java:154)
keycloak-1 | at org.keycloak.authentication.AuthenticationProcessor.authenticationAction(AuthenticationProcessor.java:986)
keycloak-1 | at org.keycloak.services.resources.LoginActionsService.processFlow(LoginActionsService.java:378)
keycloak-1 | at org.keycloak.services.resources.LoginActionsService.processResetCredentials(LoginActionsService.java:717)
keycloak-1 | at org.keycloak.services.resources.LoginActionsService.resetCredentials(LoginActionsService.java:507)
keycloak-1 | at org.keycloak.services.resources.LoginActionsService.resetCredentialsPOST(LoginActionsService.java:423)
keycloak-1 | at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
keycloak-1 | at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
keycloak-1 | at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
keycloak-1 | at java.base/java.lang.reflect.Method.invoke(Method.java:568)
keycloak-1 | at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:154)
keycloak-1 | at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:118)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:560)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:452)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:413)
keycloak-1 | at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:415)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:378)
keycloak-1 | at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:174)
keycloak-1 | at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:131)
keycloak-1 | at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:33)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:429)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:240)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:154)
keycloak-1 | at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:157)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:229)
keycloak-1 | at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:82)
keycloak-1 | at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:147)
keycloak-1 | at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:84)
keycloak-1 | at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:44)
keycloak-1 | at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284)
keycloak-1 | at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:177)
keycloak-1 | at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
keycloak-1 | at io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers$1.handle(HttpServerCommonHandlers.java:58)
keycloak-1 | at io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers$1.handle(HttpServerCommonHandlers.java:36)
keycloak-1 | at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284)
keycloak-1 | at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:177)
keycloak-1 | at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
keycloak-1 | at org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter.lambda$createBlockingHandler$0(QuarkusRequestFilter.java:82)
keycloak-1 | at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:576)
keycloak-1 | at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
keycloak-1 | at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
keycloak-1 | at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
keycloak-1 | at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
keycloak-1 | at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
keycloak-1 | at java.base/java.lang.Thread.run(Thread.java:840)
keycloak-1 |
keycloak-1 | 2024-03-19 17:55:08,076 WARN [org.keycloak.events] (executor-thread-14) type=RESET_PASSWORD_ERROR, realmId=42684046-75b5-40df-a4f0-14c8126051b7, clientId=account-console, userId=null, ipAddress=192.168.65.1, error=invalid_user_credentials, auth_method=openid-connect, auth_type=code, redirect_uri=http://localhost:8024/realms/master/account/#/, code_id=4cb73fcc-d451-40c5-8d33-cf108ef9b66c
mailhog-1 | [APIv1] KEEPALIVE /api/v1/events
mailhog-1 | [APIv1] POST /api/v1/messages
mailhog-1 | [APIv2] GET /api/v2/messages
legacy-system-example-1 | 2024-03-19T17:55:36.415Z INFO 1 --- [nio-8080-exec-9] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:36,437 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:36.445Z INFO 1 --- [io-8080-exec-10] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:36,449 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:36.453Z INFO 1 --- [nio-8080-exec-2] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:36,460 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:36.468Z INFO 1 --- [nio-8080-exec-4] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:36,472 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:36.476Z INFO 1 --- [nio-8080-exec-6] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:36,480 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:36.482Z INFO 1 --- [nio-8080-exec-9] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:36,491 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-14) User not found in external repository: [email protected]
legacy-system-example-1 | 2024-03-19T17:55:36.497Z INFO 1 --- [io-8080-exec-10] d.c.l.a.i.services.UserMigrationService : Getting migration data for: [email protected]
keycloak-1 | 2024-03-19 17:55:36,499 INFO [com.danielfrak.code.keycloak.providers.rest.remote.UserModelFactory] (executor-thread-14) Creating user model for: Lucy
keycloak-1 | 2024-03-19 17:55:36,504 WARN [org.keycloak.services] (executor-thread-14) KC-SERVICES0013: Failed authentication: java.lang.IllegalStateException: Local and remote users differ: [lucy != Lucy]
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.remote.UserModelFactory.validateUsernamesEqual(UserModelFactory.java:109)
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.remote.UserModelFactory.create(UserModelFactory.java:71)
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.LegacyProvider.lambda$getUserModel$1(LegacyProvider.java:60)
keycloak-1 | at java.base/java.util.Optional.map(Optional.java:260)
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.LegacyProvider.getUserModel(LegacyProvider.java:60)
keycloak-1 | at com.danielfrak.code.keycloak.providers.rest.LegacyProvider.getUserByEmail(LegacyProvider.java:168)
keycloak-1 | at org.keycloak.storage.UserStorageManager.lambda$getUserByEmail$20(UserStorageManager.java:398)
keycloak-1 | at org.keycloak.utils.ServicesUtils.lambda$timeBoundOne$1(ServicesUtils.java:83)
keycloak-1 | at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
keycloak-1 | at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
keycloak-1 | at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
keycloak-1 | at java.base/java.util.stream.SortedOps$RefSortingSink.end(SortedOps.java:400)
keycloak-1 | at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
keycloak-1 | at java.base/java.util.stream.Sink$ChainedReference.end(Sink.java:258)
keycloak-1 | at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:528)
keycloak-1 | at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513)
keycloak-1 | at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
keycloak-1 | at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150)
keycloak-1 | at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
keycloak-1 | at java.base/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:647)
keycloak-1 | at org.keycloak.storage.UserStorageManager.getUserByEmail(UserStorageManager.java:398)
keycloak-1 | at org.keycloak.models.cache.infinispan.UserCacheSession.getUserByEmail(UserCacheSession.java:399)
keycloak-1 | at org.keycloak.authentication.authenticators.resetcred.ResetCredentialChooseUser.action(ResetCredentialChooseUser.java:101)
keycloak-1 | at org.keycloak.authentication.DefaultAuthenticationFlow.processAction(DefaultAuthenticationFlow.java:154)
keycloak-1 | at org.keycloak.authentication.AuthenticationProcessor.authenticationAction(AuthenticationProcessor.java:986)
keycloak-1 | at org.keycloak.services.resources.LoginActionsService.processFlow(LoginActionsService.java:378)
keycloak-1 | at org.keycloak.services.resources.LoginActionsService.processResetCredentials(LoginActionsService.java:717)
keycloak-1 | at org.keycloak.services.resources.LoginActionsService.resetCredentials(LoginActionsService.java:507)
keycloak-1 | at org.keycloak.services.resources.LoginActionsService.resetCredentialsPOST(LoginActionsService.java:423)
keycloak-1 | at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
keycloak-1 | at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
keycloak-1 | at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
keycloak-1 | at java.base/java.lang.reflect.Method.invoke(Method.java:568)
keycloak-1 | at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:154)
keycloak-1 | at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:118)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:560)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:452)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:413)
keycloak-1 | at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:415)
keycloak-1 | at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:378)
keycloak-1 | at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:174)
keycloak-1 | at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:131)
keycloak-1 | at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:33)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:429)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:240)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:154)
keycloak-1 | at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:321)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:157)
keycloak-1 | at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:229)
keycloak-1 | at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:82)
keycloak-1 | at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:147)
keycloak-1 | at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:84)
keycloak-1 | at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:44)
keycloak-1 | at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284)
keycloak-1 | at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:177)
keycloak-1 | at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
keycloak-1 | at io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers$1.handle(HttpServerCommonHandlers.java:58)
keycloak-1 | at io.quarkus.vertx.http.runtime.options.HttpServerCommonHandlers$1.handle(HttpServerCommonHandlers.java:36)
keycloak-1 | at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284)
keycloak-1 | at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:177)
keycloak-1 | at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
keycloak-1 | at org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter.lambda$createBlockingHandler$0(QuarkusRequestFilter.java:82)
keycloak-1 | at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:576)
keycloak-1 | at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
keycloak-1 | at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
keycloak-1 | at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
keycloak-1 | at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
keycloak-1 | at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
keycloak-1 | at java.base/java.lang.Thread.run(Thread.java:840)
keycloak-1 |
keycloak-1 | 2024-03-19 17:55:36,506 WARN [org.keycloak.events] (executor-thread-14) type=RESET_PASSWORD_ERROR, realmId=42684046-75b5-40df-a4f0-14c8126051b7, clientId=security-admin-console, userId=null, ipAddress=192.168.65.1, error=invalid_user_credentials, auth_method=openid-connect, auth_type=code, redirect_uri=http://localhost:8024/admin/master/console/#/master/authentication/policies, code_id=7ddd7aec-3198-4941-9a23-17352f6fedb5
mailhog-1 | [APIv1] POST /api/v1/messages
mailhog-1 | [APIv2] GET /api/v2/messages
legacy-system-example-1 | 2024-03-19T17:56:10.223Z INFO 1 --- [nio-8080-exec-2] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:56:10,243 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-16) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:56:10.255Z INFO 1 --- [nio-8080-exec-4] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:56:10,257 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-16) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:56:10.260Z INFO 1 --- [nio-8080-exec-6] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:56:10,262 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-16) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:56:10.280Z INFO 1 --- [nio-8080-exec-9] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:56:10,285 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-16) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:56:10.289Z INFO 1 --- [io-8080-exec-10] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:56:10,291 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-16) User not found in external repository: lucy
legacy-system-example-1 | 2024-03-19T17:56:10.295Z INFO 1 --- [nio-8080-exec-2] d.c.l.a.i.services.UserMigrationService : Getting migration data for: lucy
keycloak-1 | 2024-03-19 17:56:10,296 WARN [com.danielfrak.code.keycloak.providers.rest.LegacyProvider] (executor-thread-16) User not found in external repository: lucy
keycloak-1 | 2024-03-19 17:56:10,318 WARN [org.keycloak.events] (executor-thread-16) type=LOGIN_ERROR, realmId=42684046-75b5-40df-a4f0-14c8126051b7, clientId=security-admin-console, userId=null, ipAddress=192.168.65.1, error=user_not_found, auth_method=openid-connect, auth_type=code, redirect_uri=http://localhost:8024/admin/master/console/#/master/authentication/policies, code_id=6d40cb3e-7892-4ae7-962c-180daced18ea, username=lucy
Thank you for doing the work to document the issue and reproduce it!
From what I see in the logs, the first and last test cases are fine - the user is not found at all, so nothing is created in Keycloak. Indeed, though, it seems that the two "reset password" scenarios do find the user by e-mail, then, upon creating them, Keycloak returns the username with an incorrect case.
This will need some investigating to find the root cause of the case change, so that it can (hopefully) be fixed without workarounds. Unfortunately, I can't guarantee when I'll have the time to sit down with the code and provide a fix.
If anyone else feels like it, though, more help is always appreciated :)
A (very) quick Google led me to this Stack Overflow issue:
https://stackoverflow.com/questions/69553369/how-to-access-the-original-case-sensitive-username-input-in-custom-user-storage
Which points to this piece of Keycloak code:
https://github.com/keycloak/keycloak/blob/bfce612641a70e106b20b136431f0e4046b5c37f/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java#L243
It seems like converting all usernames to lowercase in the plugin might be the correct way to go...
Edit:
Apparently, "Case-sensitive usernames and emails are currently not supported":
https://keycloak.discourse.group/t/use-upcase-and-lowcase-for-username-and-email-fields-in-create-users/23462/2
At least the fix should be trivial, then, once I (or anyone else) get around to it.
Any progress on this? We are having the same issue with wordpress usernames that have uppercase letters
Unfortunately, I didn't have the time to fix this yet.
However, I encourage you to create a Pull Request with the fix, if it's a pressing issue :)
As a local work-around: I added a small REST service in between the legacy system and Keycloak. There the usernames are lower-cases before sending them to the keycloak-user-migration User Federation.
Hence, the following happens:
- Someone enters Alice123 as username
- keycloak-user-migration calls GET /auth/alice123 (already lower-cased)
- (case insensitive search for Alice123 in the legacy system)
- respond with
{"username":"alice123",…}