Backgorund : I am trying to add UserStorageSPI for my legacy application, so that we can use existing user credentials to log in. I have followed this tutorial, and the full source code for the sample application is available here.
This application is storing plaintext passwords and compares them directly. However, my legacy database stores the passwords in an encrypted format. To test a sample user, I stored the credentials in bcrypt format and I overrode the following method (isValid(…)), in my CustomUserStorageProvider class.
Approach 1:
public class CustomUserStorageProvider implements UserStorageProvider, UserLookupProvider, CredentialInputValidator, UserQueryProvider { .... @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { log.info("[I57] isValid(realm={},user={},credentialInput.type={})",realm.getName(), user.getUsername(), credentialInput.getType()); if( !this.supportsCredentialType(credentialInput.getType())) { return false; } StorageId sid = new StorageId(user.getId()); String username = sid.getExternalId(); try ( Connection c = DbUtil.getConnection(this.model)) { log.info("Username to query :: " + username); PreparedStatement st = c.prepareStatement("select password from users where username = ?"); st.setString(1, username); st.execute(); ResultSet rs = st.getResultSet(); log.info("RS " + rs); if ( rs.next()) { String pwd = rs.getString(1); log.info("Password coming from query :: " + pwd); log.info("Password coming from user input :: " + credentialInput.getChallengeResponse()); PasswordEncoder enc = new BCryptPasswordEncoder(); log.info(" enc.matches(pwd, credentialInput.getChallengeResponse()) " + enc.matches(pwd, credentialInput.getChallengeResponse())); return enc.matches(pwd, credentialInput.getChallengeResponse()); } else { return false; } } catch(SQLException ex) { throw new RuntimeException("Database error:" + ex.getMessage(),ex); } } }
The keycloak can read the custom provider when I put the jar in the relevant path. I can also see keycloak is showing all the imported users from this new database, however, I get the following exception in keycloak logs, while the code is trying to compare the passwords.
2022-06-24 15:53:30,566 INFO [com.test.spi.provider.CustomUserStorageProvider] (executor-thread-2) RS org.postgresql.jdbc.PgResultSet@75dadacb 2022-06-24 15:53:30,566 INFO [com.test.spi.provider.CustomUserStorageProvider] (executor-thread-2) Password coming from query :: $2a$12$rrtl/vDlCCF0cK0aKu5H6uW30B4fp9cIrTQlHPMayQTR9ToZicEoW 2022-06-24 15:53:30,566 INFO [com.test.spi.provider.CustomUserStorageProvider] (executor-thread-2) Password coming from user input :: test 2022-06-24 15:53:30,567 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-2) Uncaught server error: java.lang.NoClassDefFoundError: org/springframework/security/crypto/password/PasswordEncoder at com.test.spi.provider.CustomUserStorageProvider.isValid(CustomUserStorageProvider.java:147) at org.keycloak.credential.UserCredentialStoreManager.lambda$validate$4(UserCredentialStoreManager.java:164) at java.base/java.util.Collection.removeIf(Collection.java:576) at org.keycloak.credential.UserCredentialStoreManager.validate(UserCredentialStoreManager.java:164) at org.keycloak.credential.UserCredentialStoreManager.isValid(UserCredentialStoreManager.java:151) at org.keycloak.credential.UserCredentialStoreManager.isValid(UserCredentialStoreManager.java:110) at org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator.validatePassword(AbstractUsernameFormAuthenticator.java:229) at org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator.validateUserAndPassword(AbstractUsernameFormAuthenticator.java:150) at org.keycloak.authentication.authenticators.browser.UsernamePasswordForm.validateForm(UsernamePasswordForm.java:55) at org.keycloak.authentication.authenticators.browser.UsernamePasswordForm.action(UsernamePasswordForm.java:48) at org.keycloak.authentication.DefaultAuthenticationFlow.processAction(DefaultAuthenticationFlow.java:169) at org.keycloak.authentication.AuthenticationProcessor.authenticationAction(AuthenticationProcessor.java:990) at org.keycloak.services.resources.LoginActionsService.processFlow(LoginActionsService.java:321) at org.keycloak.services.resources.LoginActionsService.processAuthentication(LoginActionsService.java:292) at org.keycloak.services.resources.LoginActionsService.authenticate(LoginActionsService.java:276) at org.keycloak.services.resources.LoginActionsService.authenticateForm(LoginActionsService.java:349) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:170) at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:130) at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:660) at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:524) at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:474) at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364) at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:476) at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:434) at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:192) at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:141) at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:32) at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:492) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:261) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:161) at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364) at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:164) at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:247) at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:73) at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:151) at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:82) at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:42) at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1212) at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:163) at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141) at io.quarkus.vertx.http.runtime.StaticResourcesRecorder$2.handle(StaticResourcesRecorder.java:67) at io.quarkus.vertx.http.runtime.StaticResourcesRecorder$2.handle(StaticResourcesRecorder.java:55) at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1212) at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:163) at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141) at io.quarkus.vertx.http.runtime.VertxHttpRecorder$5.handle(VertxHttpRecorder.java:380) at io.quarkus.vertx.http.runtime.VertxHttpRecorder$5.handle(VertxHttpRecorder.java:358) at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1212) at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:163) at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141) at org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter.lambda$createBlockingHandler$1(QuarkusRequestFilter.java:71) at io.vertx.core.impl.ContextImpl.lambda$null$0(ContextImpl.java:159) at io.vertx.core.impl.AbstractContext.dispatch(AbstractContext.java:100) at io.vertx.core.impl.ContextImpl.lambda$executeBlocking$1(ContextImpl.java:157) at io.quarkus.vertx.core.runtime.VertxCoreRecorder$13.runWith(VertxCoreRecorder.java:543) at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478) at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29) at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.base/java.lang.Thread.run(Thread.java:833) Caused by: java.lang.ClassNotFoundException: org.springframework.security.crypto.password.PasswordEncoder at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520) at io.quarkus.bootstrap.runner.RunnerClassLoader.loadClass(RunnerClassLoader.java:107) at io.quarkus.bootstrap.runner.RunnerClassLoader.loadClass(RunnerClassLoader.java:57) ... 65 more
I also tried adding PasswordEncoder as Bean in startup application and added it as Autowired dependency in CustomUserStorageProvider class but I get the same exception.
Approach 2 :
@SpringBootApplication public class SpiApplication { public static void main(String[] args) { SpringApplication.run(SpiApplication.class, args); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ----- @Service public class CustomUserStorageProvider implements UserStorageProvider, UserLookupProvider, CredentialInputValidator, UserQueryProvider { @Autowired PasswordEncoder enc ;
My pom.xml dependencies are here :
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-core</artifactId> <version>18.0.0</version> </dependency> <!-- User Storage SPI dependency --> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-server-spi</artifactId> <version>18.0.0</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-server-spi</artifactId> <version>18.0.0</version> </dependency>
<build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> </plugin> </plugins> </pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <shadedArtifactAttached>true</shadedArtifactAttached> </configuration> </execution> </executions> </plugin> </plugins> </build>
I am using this command to generate the jar file and keep it inside keycloak-18.0.0/providers/ package.
mvn package
Please assist me with the correct approach. I have gone through several blog posts and questions on this forum as well, but have not obtained the proper solution. TIA.
Main issue: I even setup this project in eclipse and tried exporting the project as jar file (runnable jar/jar both). The far jar generated is huge in size. 32 MBs instead of 16KBs created by shade plugin but the exception remains the same. Why is this dependency not getting added in the classpath of the jar? I cannot make the 3rd party application using this jar add the classpath, so it has to be embedded inside the final jar. Please help. I have spend a lot of time and I am running out to new workarounds to even try.
Advertisement
Answer
Thankfully I found this post and it worked for me.
https://github.com/keycloak/keycloak/issues/10230
To summarize,
Add scope as
org.keycloak keycloak-server-spi ${keycloak.version} provided org.postgresql postgresql 42.3.6 providedprovided
for keycloak, login, Postgres, or any dependencies which keycloak might already have. This is how my updated maven looks like,<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.36</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> <version>5.7.1</version> </dependency>
Utilize the maven assembly plugin and execute it using the below command. Helpful post for that another answer. Command :
mvn clean compile assembly:single
Plugin :
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.3.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <finalName>my-user-provider</finalName> <appendAssemblyId>false</appendAssemblyId> </configuration> </plugin> </plugins> </build>
Above 2 modifications, repaired my thing.