Search code examples
access-tokenspring-security-6

OAuth2AuthenticatedPrincipal not loaded after introspect is executed


I have created a Spring Security 6 project with opaque token configuration implemented in resource server. I already have an authorization server deployed both to test/prod environments. My introspector is invoked from a custom class below:

@Slf4j
public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private OpaqueTokenIntrospector delegate;

    public CustomAuthoritiesOpaqueTokenIntrospector(String oauthServerUrl, String clientId, String clientSecret) {
        this.delegate = new NimbusOpaqueTokenIntrospector(oauthServerUrl, clientId, clientSecret);
    }

    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
        return new DefaultOAuth2AuthenticatedPrincipal(
                principal.getName(), principal.getAttributes(), extractAuthorities(principal));
    }

    private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
        List<String> scopes = principal.getAttribute("scope");
        return scopes.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

which I copied from this documentation

Here is my resource server config:

@Configuration
@EnableWebSecurity
public class ResourceServerConfiguration {
    @Value("${spring.profiles.active}")
    private String activeProfile;

    @Value("${root.url.osb.basic.auth}")
    private String osbBasicAuth;

    @Value("${auth.server.url}")
    private String oauthServerUrl;

    @Value("${auth.server.clientId}")
    private String clientId;

    @Value("${auth.server.clientsecret}")
    private String clientSecret;

    public static final String BEARER_PREFIX = "Bearer ";
    public static final String HEADER_NAME = "Authorization";
    private final UserService userService;

    public ResourceServerConfiguration(UserService userService) {
        this.userService = userService;
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain filterChain)
                    throws ServletException, IOException {
                // We don't want to allow access to a resource with no token so clear
                // the security context in case it is actually an OAuth2Authentication
                var authHeader = request.getHeader(HEADER_NAME);
                if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, BEARER_PREFIX)) {
                    SecurityContextHolder.clearContext();
                }
                filterChain.doFilter(request, response);
            }
        }, AbstractPreAuthenticatedProcessingFilter.class);
//        http.csrf().disable();
        http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/", "/v1/open/**").permitAll());

        http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/pension/**").authenticated())
                .httpBasic(Customizer.withDefaults());

        if (StringUtils.equalsIgnoreCase(activeProfile, "dev")) {
            http.authorizeHttpRequests(authorize ->
                authorize.requestMatchers("/webjars/**", "/resources/**", "/swagger-ui.html", "/swagger-resources/**", "/v2/api-docs", "index.html")
                .permitAll());
        }

        http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2
                        .opaqueToken(Customizer.withDefaults())
                );
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        String[] authParts = osbBasicAuth.split(":");
        String username = authParts[0];
        String password = authParts[1];

        UserDetails userDetails = User.builder()
                .username(username)
                .password(new BCryptPasswordEncoder().encode(password)).roles()
                .build();
        return new InMemoryUserDetailsManager(userDetails);
    }

    @Bean
    public OpaqueTokenIntrospector introspector() {
        return new CustomAuthoritiesOpaqueTokenIntrospector(oauthServerUrl + "check_token", clientId, clientSecret);
    }
}

For this process, token is fetched from the authorization server, via link http://localhost:9696/oauth/token which is called separately, in my case via Postman. After I get a token, I call another service by pasting this token to the authorization header.

For example, I call this endpoint: http://localhost:9090/v1/lead-management/get-details/18410 and this endpoint is private and executes only with tokens. After I execute it, the first thing happens is my introspect is triggered by calling http://localhost:9696/oauth/http://localhost:9696/oauth/check_token in order to validate my token, and then it calls the final endpoint. The token validation is successful, I got all details necessary to prove that my user is authenticated: here

However, the final endpoint throws 500 error. And here I don't have a clue what the problem is about and it seems to me that Principal wasn't mapped or loaded correctly.

I have been dealing with this mess for about 1 month now and still can't seem to find a solution. I hope you can help to solve this problem. Thanks!

-- UPDATE --

Since some of you mentioned about error logs for 500 error, I decided to put logs in here, so you can see that these logs also don't give much information about the source of a problem.

Securing GET /v1/lead-management/get-details/18410
07-11-2024 09:29:55.723 http-nio-9090-exec-1 [correlationId: ] [DEBUG] CompositeLog - HTTP POST http://localhost:9696/oauth/check_token
07-11-2024 09:29:55.732 http-nio-9090-exec-1 [correlationId: ] [DEBUG] InternalLoggerFactory - Using SLF4J as the default logging framework
07-11-2024 09:29:55.734 http-nio-9090-exec-1 [correlationId: ] [DEBUG] CompositeLog - Accept=[text/plain, application/json, application/*+json, */*] 07-11-2024 09:29:55.734 http-nio-9090-exec-1 [correlationId: ] [DEBUG]  CompositeLog - Writing [{token=[3055687b-6c25-4a59-894e-3a209ce3f1d7]}] with org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter 07-11-2024 09:29:55.748 http-nio-9090-exec-1 [correlationId: ] [DEBUG] LoggingProviderImpl$JULWrapper - sun.net.www.MessageHeader@33d105118 pairs: {POST /oauth/check_token HTTP/1.1: null}{Accept: application/json}{Content-Type: application/x-www-form-urlencoded;charset=UTF-8}{Authorization: Basic VVNFUl9DTElFTlRfUlM6dGVzdA==}{User-Agent: Java/17.0.7}{Host: localhost:9696}{Connection: keep-alive}{Content-Length: 42}
07-11-2024 09:29:55.935 http-nio-9090-exec-1 [correlationId: ] [DEBUG] LoggingProviderImpl$JULWrapper -
                    sun.net.www.MessageHeader@4978153910 pairs: {null: HTTP/1.1 200}{X-Content-Type-Options: nosniff}{X-XSS-Protection: 1; mode=block}{Cache-Control: no-cache, no-store, max-age=0, must-revalidate}{Pragma: no-cache}{Expires: 0}{X-Frame-Options: DENY}{Content-Type: application/json;charset=UTF-8}{Transfer-Encoding: chunked}{Date: Thu, 11 Jul 2024 04:29:55 GMT}
07-11-2024 09:29:55.936 http-nio-9090-exec-1 [correlationId: ] [DEBUG] CompositeLog -
                    Response 200 OK
07-11-2024 09:29:55.937 http-nio-9090-exec-1 [correlationId: ] [DEBUG] CompositeLog -
                    Reading to [java.lang.String] as "application/json;charset=UTF-8"
07-11-2024 09:29:56.002 http-nio-9090-exec-1 [correlationId: ] [DEBUG] OpaqueTokenAuthenticationProvider -
                    Authenticated token
07-11-2024 09:29:56.002 http-nio-9090-exec-1 [correlationId: ] [DEBUG] BearerTokenAuthenticationFilter -
                    Set SecurityContextHolder to BearerTokenAuthentication [Principal=org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal@4879b039, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[role_admin, role_user]]
07-11-2024 09:29:56.006 http-nio-9090-exec-1 [correlationId: ] [DEBUG] FilterChainProxy -
                    Secured GET /v1/lead-management/get-details/18410
07-11-2024 09:29:56.016 http-nio-9090-exec-1 [correlationId: ] [DEBUG] FilterChainProxy -
                    Securing GET /error
07-11-2024 09:29:56.017 http-nio-9090-exec-1 [correlationId: ] [DEBUG] FilterChainProxy -
                    Secured GET /error
07-11-2024 09:29:56.019 http-nio-9090-exec-1 [correlationId: ] [DEBUG] LogFormatUtils -
                    "ERROR" dispatch for GET "/error", parameters={}
07-11-2024 09:29:56.021 http-nio-9090-exec-1 [correlationId: ] [DEBUG] AbstractHandlerMapping -
                    Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
07-11-2024 09:29:56.022 http-nio-9090-exec-1 [correlationId: ] [DEBUG] OpenEntityManagerInViewInterceptor -
                    Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
07-11-2024 09:29:56.052 http-nio-9090-exec-1 [correlationId: ] [DEBUG] AbstractMessageConverterMethodProcessor -
                    Using 'application/json', given [*/*] and supported [application/json, application/*+json]
07-11-2024 09:29:56.054 http-nio-9090-exec-1 [correlationId: ] [DEBUG] LogFormatUtils -
                    Writing [{timestamp=Thu Jul 11 09:29:56 ORAT 2024, status=500, error=Internal Server Error, path=/v1/lead-man (truncated)...]
07-11-2024 09:29:56.098 http-nio-9090-exec-1 [correlationId: ] [DEBUG] OpenEntityManagerInViewInterceptor -
                    Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
07-11-2024 09:29:56.099 http-nio-9090-exec-1 [correlationId: ] [DEBUG] FrameworkServlet -
                    Exiting from "ERROR" dispatch, status 500

Solution

  • After some tweaks by my teamlead, it finally worked. Using custom introspector class was removed and resource server configuration was modified to this:

    http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2
        .opaqueToken(opaqueTokenConfigurer -> opaqueTokenConfigurer
        .introspectionUri(oauthServerUrl + "check_token")
        .introspectionClientCredentials(clientId, clientSecret)
      )
    );
    

    OpaqueTokenIntrospector bean was also removed