Search code examples
springspring-security

Why in Spring application I get null as user when using @AuthenticationPrincipal?


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?


Solution

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