I'm using Spring connecting to KeyCloak to provide authentication in my application, but ran into a problem. Here is a part of my code:
@GetMapping("/api/user/name")
public ResponseEntity<?> getUserName(@AuthenticationPrincipal OidcUser user) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("Authentication: " + authentication);
if (user != null) {
return ResponseEntity.ok(user);
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("User not authenticated");
}
}
When calling this method, I have the user variable always set to null. I expect to get information about the authenticated user. Here is the configuration of my Spring application:
@SpringBootApplication
public class TestOauth2Application {
public static void main(String[] args) {
SpringApplication.run(TestOauth2Application.class, args);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
http.oauth2Login(Customizer.withDefaults());
http.logout(logout -> logout
.logoutSuccessUrl("/")
.permitAll());
return http
.authorizeHttpRequests(c -> c
.requestMatchers("/error").permitAll()
.requestMatchers("/manager.html").hasRole("USER")
.requestMatchers("/test/test").hasRole("USER")
.anyRequest().authenticated())
.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
var converter = new JwtAuthenticationConverter();
var jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
converter.setPrincipalClaimName("sub");
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
var authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
var roles = jwt.getClaimAsStringList("spring_sec_roles");
return Stream.concat(authorities.stream(),
roles.stream()
.filter(role -> role.startsWith("ROLE_"))
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast))
.toList();
});
return converter;
}
@Bean
public OAuth2UserService<OidcUserRequest, OidcUser> oAuth2UserService() {
var oidcUserService = new OidcUserService();
return userRequest -> {
var oidcUser = oidcUserService.loadUser(userRequest);
var roles = oidcUser.getClaimAsStringList("spring_sec_roles");
var authorities = Stream.concat(oidcUser.getAuthorities().stream(),
roles.stream()
.filter(role -> role.startsWith("ROLE_"))
.map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast))
.toList();
var nameAttribute = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
var name = oidcUser.getAttribute(nameAttribute);
oidcUser.getAttributes().put("name", name);
return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
};
}
}
And I'm sending the next token:
Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ6eTdPbVN1b3NTVGJCUlhEYnRzZFBtOUtDV3FBQWZhQy1iMzh6RHBVOTlVIn0.eyJleHAiOjE3MDM3MjM2NDEsImlhdCI6MTcwMzcyMjc0MSwiYXV0aF90aW1lIjoxNzAzNzIyNzQxLCJqdGkiOiI3YmE0YmQ2NC1mMDQ5LTRjMjgtYjBkMS1jODQ3NWEyNWIwYTUiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwOTAvcmVhbG1zL2thc2lhIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjJhYTYyM2Y4LTI3NTEtNDA1Mi05ZDBkLTNmMWQwNzhiNmU5MCIsInR5cCI6IkJlYXJlciIsImF6cCI6Imthc2lhLXNlY3VyaXR5Iiwibm9uY2UiOiJiYzUxYWE5MC0zMTA0LTQ5ZjEtOGNmYy01ZDM4N2NlOTE4NGQiLCJzZXNzaW9uX3N0YXRlIjoiNjIxMThmMTUtNDE1OC00YzlmLWEyZDItZTNmYzRiMGIwMThhIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJodHRwOi8vbG9jYWxob3N0OjMwMDAvIiwiaHR0dHA6Ly9sb2NhbGhvc3Q6MzAwMCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1rYXNpYSIsIlJPTEVfVVNFUiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJVU0VSIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtbWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUgIiwic2lkIjoiNjIxMThmMTUtNDE1OC00YzlmLWEyZDItZTNmYzRiMGIwMThhIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiQXJ0ZW0gRHViZW5rbyIsInNwcmluZ19zZWNyZXRfaWQiOiI2MjExOGYxNS00MTU4LTRjOWYtYTJkMi1lM2ZjNGIwYjAxOGEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImNvbnRhY3Qtb3JpZ2lucyI6W3sicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfV0sInNjb3BlIjoiTm9ybWFsIEFwcCIsInNpZCI6IjYyMTE4ZjE1LTQxNTgtNGM5Zi1hMmQyLWUzZmM0YjBiMDE4YSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkFydGVtIER1YmVua28iLCJzcHJpbmdfc2VjX3JvbGVzIjpbImRlZmF1bHQtcm9sZXMta2FzaWEiLCJST0xFX1VTRVIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiVVNFUiJdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhcnRlbS5kdWJlbmtvMDMwOEBnbWFpbC5jb20iLCJnaXZlbl9uYW1lI
UPD.
But at the same time, Authentication authentication = SecurityContextHolder.getContext().getAuthentication()
works for me.
Maybe I don't fully understand something?
The actual type you get with Authentication authentication = SecurityContextHolder.getContext().getAuthentication()
will change depending on how you authorize your request (a browser session with login or a Bearer token in a request from a REST client).
OidcUser
is the principal of OAuth2AuthenticationToken
for an authenticated user on an OAuth2 client with oauth2Login
in an OIDC environment (which is the case with Keycloak when you configure the provider.issuer-uri
and set the openid
scope on the authorization request).
Requests to OAuth2 clients with oauth2Login
are authorized using a session cookie (not with a Bearer
token).
Jwt
is the principal of JwtAuthenticationToken
which is the default implementation for a request to a resource server with a JWT decoder.
Requests to OAuth2 resource servers are authorized using a Bearer
access token in the Authorization
header.
Mixing configuration with oauth2Login
and oauth2ResourceServer
in the same security filter chain is a very bad idea. OAuth2 clients with oauth2Login
and OAuth2 resource servers are different things and have very different security requirements (Spring Boot provides with different starters for a reason):
oauth2Login
: security based on sessions, requires protection against CSRF and it is generally expected that unauthorized requests to protected resources are redirected to login (302
status code)oauth2ResourceServer
: security based on access token, can (should?) be stateless (no session), insensible to CSRF, and it is expected that unauthorized requests to protected resources are answered 401
(login makes no sense on resource server)I don't see a valid use case for having both flavors of security on a single endpoint.