Search code examples
javaspring-bootspring-securityspring-security-oauth2

SpringOpaqueTokenIntrospector Class Not Found


I've been facing some issues while running Spring Boot 2.7.0 and spring-boot-starter-oauth2-resource-server. My goal is to use Keycloak to check tokens from http requests. But since I've added spring-boot-starter-oauth2-resource-server to my application it fails to start because of exception:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerOpaqueTokenConfiguration$OpaqueTokenIntrospectionClientConfiguration': Lookup method resolution failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerOpaqueTokenConfiguration$OpaqueTokenIntrospectionClientConfiguration] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@6f94fa3e]
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:289)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors(AbstractAutowireCapableBeanFactory.java:1302)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1219)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)
    at com.test.TestApplication.main(TestApplication.java:14)
    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.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)
Caused by: java.lang.IllegalStateException: Failed to introspect Class [org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerOpaqueTokenConfiguration$OpaqueTokenIntrospectionClientConfiguration] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@6f94fa3e]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:485)
    at org.springframework.util.ReflectionUtils.doWithLocalMethods(ReflectionUtils.java:321)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:267)
    ... 23 common frames omitted
Caused by: java.lang.NoClassDefFoundError: org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector
    at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
    at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3402)
    at java.base/java.lang.Class.getDeclaredMethods(Class.java:2504)
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:467)
    ... 25 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector
    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)
    ... 29 common frames omitted


Process finished with exit code 0

Dependencies of project:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ws</groupId>
    <artifactId>spring-ws-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

Do you have any idea what is wrong with this setup? Thanks in advance


Solution

  • First, if your authorization-server produces JWTs (Keycloak does), you'd probably better use JWT decoder than token introspection. This is much more efficient: all JWTs are validated with a public key downloaded only once when introspection requires resource-server (Spring REST API) to submit each and every access-token to authorization-server (Keycloak).

    Second, double check the spring-security-oauth2-resource-server version in your dependency tree: SpringOpaqueTokenIntrospector is there from 5.6 only.

    Third, I contributed an easier way to map authorities for token introspection in version 5.8: provide an authentication converter (handle the introspection result) instead of overloading the all introspector:

    http.oauth2ResourceServer().opaqueToken().authenticationConverter(authenticationConverter);
    

    where authenticationConverter can be implemented as:

    (String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> new BearerTokenAuthentication(
                authenticatedPrincipal,
                new OAuth2AccessToken(
                        OAuth2AccessToken.TokenType.BEARER,
                        introspectedToken,
                        authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.IAT),
                        authenticatedPrincipal.getAttribute(OAuth2TokenIntrospectionClaimNames.EXP)),
                authoritiesConverter.convert(authenticatedPrincipal.getAttributes()));
    

    and authoritiesConverter is a Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> (maps authorities from Keycloak introspection JSON response, which you should have already implemented)

    Last, you might find useful tips for configuring your resource-server in this tutorials: it could be as simple as

            <dependency>
                <groupId>com.c4-soft.springaddons</groupId>
                <!-- change "webmvc" with "webflux" for reactive apps -->
                <!-- change "jwt" with "introspecting" for access-token introspection instead of JWT decoding -->
                <artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
                <!-- 6.0.4 goes with spring-boot 3.0.0-RC1, use 5.x with boot 2.6.x and below -->
                <version>6.0.4</version>
            </dependency>
    
    @EnableMethodSecurity
    @Configuration
    public class SecurityConfig { }
    
    spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://localhost:8443/realms/master/protocol/openid-connect/token/introspect
    spring.security.oauth2.resourceserver.opaquetoken.client-id=spring-addons-confidential
    spring.security.oauth2.resourceserver.opaquetoken.client-secret=change-me
    
    com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
    
    # shoud be configured with a list of private-claims this authorization-server puts user roles into
    # below is default Keycloak conf for a `spring-addons` client with client roles mapper enabled
    com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
    
    # Fine-grained CORS configuration can be set per path as follow:
    com.c4-soft.springaddons.security.cors[0].path=/sample/**
    
    # More useful resource-server security conf from preperties, use IDE auto-completion