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
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