I want to secure my Spring Backend with Keycloak. I've got a Controller which is secured and I manged to make it possible so that either you can access it via the browser and you have to login with the keycloak SSO or you can use postman and send the JWT bearer token with it.
But if I use option 1 I can't use postman, it will just return me the html of the login page. On the other hands if Im using option 2 I can only use postman and not access the endpoint with the browser. Is it somehow possible to make the endpoint accessible both ways?
Maybe @Ch4mp got any hinds?
I tried different Security Configurations, but I dont know if its even possible to mix them:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
interface AuthoritiesConverter extends Converter<Map<String, Object>, Collection<GrantedAuthority>> {}
@Bean
AuthoritiesConverter realmRolesAuthoritiesConverter() {
return claims -> {
final var realmAccess = Optional.ofNullable((Map<String, Object>) claims.get("realm_access"));
final var roles =
realmAccess.flatMap(map -> Optional.ofNullable((List<String>) map.get("roles")));
return roles.map(List::stream).orElse(Stream.empty()).map(SimpleGrantedAuthority::new)
.map(GrantedAuthority.class::cast).toList();
};
}
// @Bean
// GrantedAuthoritiesMapper authenticationConverter(
// Converter<Map<String, Object>, Collection<GrantedAuthority>> realmRolesAuthoritiesConverter) {
// return (authorities) -> authorities.stream()
// .filter(authority -> authority instanceof OidcUserAuthority)
// .map(OidcUserAuthority.class::cast).map(OidcUserAuthority::getIdToken)
// .map(OidcIdToken::getClaims).map(realmRolesAuthoritiesConverter::convert)
// .flatMap(roles -> roles.stream()).collect(Collectors.toSet());
// }
//
@Bean
JwtAuthenticationConverter authenticationConverterJWT(
Converter<Map<String, Object>, Collection<GrantedAuthority>> authoritiesConverter) {
var authenticationConverter = new JwtAuthenticationConverter();
authenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> {
return authoritiesConverter.convert(jwt.getClaims());
});
return authenticationConverter;
}
// @Bean
// SecurityFilterChain clientSecurityFilterChain(HttpSecurity http,
// ClientRegistrationRepository clientRegistrationRepository) throws Exception {
// http.oauth2Login(Customizer.withDefaults());
// http.logout((logout) -> {
// final var logoutSuccessHandler =
// new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
// logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/");
// logout.logoutSuccessHandler(logoutSuccessHandler);
// });
//
// http.authorizeHttpRequests(requests -> {
// requests.requestMatchers("/", "/favicon.ico", "/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html" ).permitAll();
//
// requests.anyRequest().authenticated();
// });
//
// return http.build();
// }
//
@Bean
SecurityFilterChain resourceServerSecurityFilterChain(
HttpSecurity http,
Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception {
http.oauth2ResourceServer(resourceServer -> {
resourceServer.jwt(jwtDecoder -> {
jwtDecoder.jwtAuthenticationConverter(authenticationConverter);
});
});
http.sessionManagement(sessions -> {
sessions.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}).csrf(csrf -> {
csrf.disable();
});
http.authorizeHttpRequests(requests -> {
requests.requestMatchers("/", "/favicon.ico", "/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html" ).permitAll();
requests.anyRequest().authenticated();
});
return http.build();
}
}
Edit: Ch4mp gave the correct solution, it is possible to use multiple security filters with different priorities. You can then check if the header contains the Bearer token and use the resource server security chain or else use the oauth2Login
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
SecurityFilterChain clientSecurityFilterChain(HttpSecurity http,
ClientRegistrationRepository clientRegistrationRepository) throws Exception {
http.oauth2Login(Customizer.withDefaults());
http.logout((logout) -> {
final var logoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/");
logout.logoutSuccessHandler(logoutSuccessHandler);
});
http.authorizeHttpRequests(requests -> {
requests.requestMatchers("/", "/favicon.ico", "/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html" ).permitAll();
requests.anyRequest().authenticated();
});
return http.build();
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE - 1)
SecurityFilterChain resourceServerSecurityFilterChain(
HttpSecurity http,
Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception {
http.securityMatcher((HttpServletRequest request) -> {
return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)).map(h -> {
return h.toLowerCase().startsWith("bearer ");
}).orElse(false);
});
http.oauth2ResourceServer(resourceServer -> {
resourceServer.jwt(jwtDecoder -> {
jwtDecoder.jwtAuthenticationConverter(authenticationConverter);
});
});
http.sessionManagement(sessions -> {
sessions.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}).csrf(csrf -> {
csrf.disable();
});
http.authorizeHttpRequests(requests -> {
requests.requestMatchers("/", "/favicon.ico", "/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html" ).permitAll();
requests.anyRequest().authenticated();
});
return http.build();
}
Use a security filter-chain for each request authorisation mechanism. In your case one for Bearer
tokens (stateless oauth2ResourceServer
with a JWT decoder) and another one for session cookies (stateful client with oauth2Login
). See this other answer for configuring apps with several filter-chains (this answer is about Basic
+ oauth2Login
, but the idea is the same: define @Order
and securityMatcher
).
REST APIs are better configured solely as stateless resource server. If you're trying to authorize REST requests from a Single-Page (Angular, React, Vue, or whatever) or mobile application, you should consider:
401 Unautorized
(instead of 302 Redirect to login
as done by default with oauth2Login
) and handle unauthorized in the frontend with a request interceptor or whatever