Keycloak refuses to return roles on request - spring-boot

My Keycloak is returning an OAuth2AuthenticationToken, but refuses to add the roles the user has. Instead, it's returning the somewhat generic:
Authority: ROLE_USER
Authority: SCOPE_email
Authority: SCOPE_openid
Authority: SCOPE_profile
The Java method is
#GetMapping
public String work_queue(Principal principal, Model model) {
...
//Principal comes in as org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
//
Object principal2 = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// principal2 is org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser
}
In Postman, I was able to convince Keycloak to return a JWT token using the Get Token functionality. Inside the JWT, after decompiling, I saw all the roles I wanted it to see. Yet somehow, the Spring Boot configuration decided to shorten this down to something much smaller.
What would someone need to know to guess a good solution?
Please see Spring Boot not fetching Keycloak Roles

You need to provide a #Bean implementing Converter<Jwt, Collection<GrantedAuthority>> to override Spring's JwtGrantedAuthoritiesConverter (it is this bean mapping scopes to authorities).
But, you could use a lib I worte for spring-boot OpenID resource-servers auto-configuration (works with any OIDC authorization-server, Keycloak included) which might save you a lot of configuration and provide you with a more OpenID oriented Authentication than JwtAuthenticationToken: OAuthentication<OpenidClaimSet>, which exposes OpenidClaimSet as principal.
It's available from maven-central and source is there: https://github.com/ch4mpy/spring-addons.
This very simple tutorial should be enough (you can refer to this other one for more advanced use-cases):
spring-boot app with those dependencies:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-security-oauth2-webmvc-addons</artifactId>
<version>4.4.7</version>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-security-oauth2-test-webmvc-addons</artifactId>
<version>4.4.7</version>
<scope>test</scope>
</dependency>
that java config
#EnableGlobalMethodSecurity(prePostEnabled = true)
public static class WebSecurityConfig {
}
those properties
# shoud be set to where your authorization-server is
com.c4-soft.springaddons.security.token-issuers[0].location=https://localhost:9443/auth/realms/master
# shoud be configured with a list of private-claims this authorization-server puts user roles into
# below is default Keycloak conf for a `spring-addons` client with client roles mapper enabled
com.c4-soft.springaddons.security.token-issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons.roles
# advanced CORS configuration can be made per API route
com.c4-soft.springaddons.security.cors[0].path=/greet/**
com.c4-soft.springaddons.security.cors[0].allowed-origins=https://localhost,https://localhost:8100,https://localhost:4200
# use IDE auto-completion or see SpringAddonsSecurityProperties javadoc for complete configuration properties list to change defaults:
# - anonymous enabled
# - empty list of permitAll() routes
# - CSRF disabled
# - stateless session management
# - case and prefix for mapped authorities
# - 401 (unauthorized) instead of 302 (redirect to login)
# - CORS allowed methods, headers, etc. for each path
Yes, with 2 dependencies, 1 configuration line and 4 properties, we just configured an OpenID resource-server with CORS and authorities mapping from random private claims (plus a few other things useful to resource servers). Could it be simpler?
As an extra bonus, it comes with annotations to configure your unit-tests security context (this is from the third dependency):
#WebMvcTest(GreetingController.class)
#AutoConfigureSecurityAddons
#Import(WebSecurityConfig.class)
class GreetingControllerTest {
#Autowired
MockMvc mockMvc;
#Test
#OpenId(authorities = { "NICE_GUY", "AUTHOR" }, claims = #OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenGrantedWithNiceGuyThenCanGreet() throws Exception {
mockMvc
.perform(get("/greet").secure(true))
.andExpect(status().isOk())
.andExpect(content().string("Hi Tonton Pirate! You are granted with: [NICE_GUY, AUTHOR]."));
}
#Test
#OpenId(authorities = { "AUTHOR" }, claims = #OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenNotGrantedWithNiceGuyThenForbidden() throws Exception {
mockMvc.perform(get("/greet").secure(true)).andExpect(status().isForbidden());
}
}
P.S.
Please give a star to https://github.com/ch4mpy/spring-addons if you find it useful.

Related

Validate the api with user defined roles in spring boot oauth resource server

I am currently working on resource server implemented by the spring boot o auth 2.0. Spring boot version would be 3.0.0. Need to authrorize the api with user-authorities. I tried below samples.
https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/opaque-token.html. But did n't get the user-authorities attribute. Can anyone sugget me the approach to to validate api with user roles in #PreAuthorize anotations.
Sample token format :
{
"sub": "admin#support.com",
"aud": "client",
"user-authorities": [
"READ_PRIVILEGE",
"WRITE_PRIVILEGE"
],
"azp": "client",
"iss": "http://localhost:8080",
"exp": 1676210945,
"iat": 1676209145
}
Two options to override default authorities mapping in case of token introspection (read user-authorities claim in your case instead of scope).
override the all OpaqueTokenIntrospector, as explained in the doc you linked
override just the OpaqueTokenAuthenticationConverter to which the default introspector delegates the conversion from successful introspection result to an Authentication instance
The second option is less intrusive as you don't take responsibility of how introspection is made (which REST client is used, how client authentication is performed, query built, response parsed, exceptions handled, etc.).
Sample usage:
#Bean
OpaqueTokenAuthenticationConverter introspectionAuthenticationConverter() {
return (String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> {
// double check the claim name to extract authorities from and if a prefix should be added:
// in the sample provided in your question, it is "user-authorities" claim
// but in the sample linked in your comment, it is "scope" claim and "SCOPE_" prefix should be added for "hasAuthority("SCOPE_openid")" to be matched
#SuppressWarnings("unchecked")
final var rolesClaim = Optional.ofNullable((List<String>) authenticatedPrincipal.getAttribute("user-authorities")).orElse(List.of());
return new BearerTokenAuthentication(
authenticatedPrincipal,
new OAuth2AccessToken(
TokenType.BEARER,
introspectedToken,
authenticatedPrincipal.getAttribute(IdTokenClaimNames.IAT),
authenticatedPrincipal.getAttribute(IdTokenClaimNames.EXP)),
// You should insert a ".map()" step to insert a prefix if access-control rules expect one (that is not visible in the access token)
rolesClaim.stream().map(SimpleGrantedAuthority::new).toList());
};
}
#Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http,
OpaqueTokenAuthenticationConverter introspectionAuthenticationConverter)
throws Exception {
http.oauth2ResourceServer().opaqueToken().authenticationConverter(introspectionAuthenticationConverter);
...
return http.build();
}
Complete sample there with one of "my" starters which make things much simpler (check security conf and properties file against what you have in yours).

Mapping OAuth2 / OpenID Access / User Tokens to Sessions in SpringBoot Apps

I am trying to learn techniques for implementing OAuth2 / OpenID Connect with servlet apps and react apps. I have Authorization Server functions working correctly in Keycloak and have commandline test authorization_code, token and refresh flows so that plumbing works. When building a MVC servlet, code to enforce required authorizations works, redirects a user's browser to Keycloak for authentication and code generation, the code is returned to my servlet which properly obtains an access token for the code. However, while redirecting the user to a "main page" in the authenticated realm, I am not correct mapping the OAuth2 layer tokens to session and SecurityContext objects used in Spring Security so the subsequent page request is treated as unauthenticated.
Here is a top level summary of the components being used:
SpringBoot 2.6.7 (latest as of 5/15/2022)
SpringBoot Thymeleaf Start
Keycloak 18.0.0 (latest as of 5/15/2022)
Java JDK 18.0.1
Key dependencies from the pom.xml (just the artifactId for brevity):
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<artifactId>spring-boot-starter-security</artifactId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<artifactId>spring-boot-starter-web</artifactId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<artifactId>spring-boot-starter-webflux</artifactId>
<artifactId>spring-integration-http</artifactId>
Here are the implementation components that are working:
Keycloak is installed, running on 192.168.99.10:8011 with a client configured for use by the app
the Keycloak client is configured for OpenID Connect protocol
curl tests of authorization_code, token and refresh queries to Keycloak all function
a servlet app has been created with four key page areas:
/myapp/gui/public --- public pages, no authentication required
/myapp/gui/access -- used for functions to handle access login and logout and Oauth callbacks
/myapp/gui/clients --- pages for authenticated users with myfirmuser permissions
/myapp/gui/admin --- pages for authenticated users with myfirmadmin permissions
redirection of unauthenticated users to /myapp/gui/access/oauthproviders page
rendering of links on that oauthproviders page to /auth endpoint of defined Authorization Servers
clicking on Keycloak displays its authentication page and sends back authorization code
the /myapp/gui/access/oath2/callback/keycloak page is handled and calls the /token endpoint on Keycloak
a token is returned and the callback page creates a JSESSION, addes the access_token and
refresh_token in the HttpServletRequest then redirects to /myapp/gui/clients/mainpage
The actual (undesired) behavior is that after receiving the new access_token from the Authorization Server, the redirect sent to the browser forwarding the human user to /myapp/gui/clients/mainpage (the logged in "home page") is then processed by the security filters and no token is found so the user is redirected back to /myapp/gui/oauthproviders to start the login process again.
Clearly, I am not correctly populating the access token or JWT session token in the SecurityContext or HttpRequest or HttpResponse object for it to go out to the browser and come back. That logic is currently implemented in my AccessController class that handles the integration to the remote AuthorizationServer (Keycloak). I've tried creating classes to invoked for AuthenticationSuccessHandler and AuthenticationFailureHandler. Here are the key classes in the build.
src/main/java/com/myfirm/dependsgui/AccessController.java
src/main/java/com/myfirm/dependsgui/DependsAuthenticationFailureHandler.java
src/main/java/com/myfirm/dependsgui/DependsAuthenticationSuccessHandler.java
src/main/java/com/myfirm/dependsgui/DependsController.java
src/main/java/com/myfirm/dependsgui/DependsguiApplication.java
src/main/java/com/myfirm/dependsgui/KeycloakAuthoritiesExtractor.java
src/main/java/com/myfirm/dependsgui/KeycloakPrincipalExtractor.java
src/main/java/com/myfirm/dependsgui/SecurityConfiguration.java
The classes aimed at transforming OAuth2 layer user / authorization information are referenced in the configure() class and logs at startup DO show them firing to point to my custom classes. However, something between OAuth and Spring Security doesn't seem to be linked correct to fire those classes after successful authentication.
Key questions:
in configure(), should oauth2ResourceServer() only be used for web service builds (not MVC apps)?
in configure(), are formLogin() and oauth2Login() mutually exclusive and not to be used together?
should the mapping of userdetails from the access token into the SecurityContext Authentication object be implemented in
a) filter layer classes?
b) an AuthenticationSuccessHandler derived class?
c) in PrincipalExtractor and AuthoritiesExtractor derived classes?
c) my servlet controller class handling login / logout actions?
d) somewhere else?
I think it has to be performed in a filter layer or AuthenticationSuccessHandler. However, the run-time flow doesn't appear to be invoking my custom classes to give me a place to trace backward to the point where I'm inevitably not doing something correctly.
Code fragments are excerpted below. Any help would be greatly appreciated.
===========================
Here is the KeycloakPrincipalExtractor class:
package com.myfirm.dependsgui;
//imports omitted for brevity
public class KeycloakPrincipalExtractor implements PrincipalExtractor {
private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName());
#Override
public Object extractPrincipal(Map<String, Object> map) {
thisLog.info("extractPrincipal() -- extracting preferred_username from Oauth token - value=" +
map.get("preferred_username").toString());
return map.get("preferred_username");
}
}
Here is the KeycloakAuthoritiesExtractor class:
package com.myfirm.dependsgui;
//imports omitted for brevity
public class KeycloakAuthoritiesExtractor implements AuthoritiesExtractor {
private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName());
// for now, just mockup three capabilities and two sets of authorities
// * everyone has MYFIRM_USER
// * full will have MYFIRM_FULL
// * get will have MYFIRM_GET
List<GrantedAuthority> MYFIRM_USER = AuthorityUtils.commaSeparatedStringToAuthorityList(
"SCOPE_myfirmuser");
List<GrantedAuthority> MYFIRM_ADMIN = AuthorityUtils.commaSeparatedStringToAuthorityList(
"SCOPE_myfirmadmin");
List<GrantedAuthority> MYFIRM_ANONYMOUS = AuthorityUtils.commaSeparatedStringToAuthorityList(
"SCOPE_myfirmanonymous");
#Override
public List<GrantedAuthority> extractAuthorities (Map<String, Object> map) {
thisLog.info("DEBUG -- extractAuthorities() - map --> " + map.toString());
if (Objects.nonNull(map.get("realm-access"))) {
if (!((LinkedHashMap) map.get("realm-access")).get("roles").equals("myfirmuser")) {
return MYFIRM_USER;
}
if (!((LinkedHashMap) map.get("realm-access")).get("roles").equals("myfirmuser")) {
return MYFIRM_ADMIN;
}
}
return MYFIRM_ANONYMOUS;
}
}
Here is the method in my AccessController.java handling the authorization response from the Authorization Server and calling the remote /token endpoint to get an access_token.
#GetMapping("/access/oauth2/callback/{oauthprovidername}")
public String oauthCallback(
#PathVariable("oauthprovidername") String oauthprovidername,
#RequestParam("session_state") String sessionstate,
#RequestParam("code") String code,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
Model model) {
thisLog.info("oauthCallback() - oauthprovidername=" + oauthprovidername +
" code=" + code + " session_state=" + sessionstate);
ClientRegistration providerRegistration = clientRegistrationRepository.findByRegistrationId(oauthprovidername);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
LinkedMultiValueMap<String,String> params = new LinkedMultiValueMap<>();
params.add("grant_type","authorization_code");
params.add("scope","openid");
params.add("client_id",providerRegistration.getClientId());
params.add("client_secret",providerRegistration.getClientSecret());
params.add("code",code);
params.add("redirect_uri",providerRegistration.getRedirectUri());
RestTemplate restTemplate = new RestTemplate();
HttpEntity< LinkedMultiValueMap<String,String> > request = new HttpEntity<>(params,headers);
ResponseEntity<String> response = restTemplate.postForEntity(
providerRegistration.getProviderDetails().getTokenUri(),
request,
String.class);
// the response has this structure:
// {"access_token":"xxx","expires_in":300,"refresh_expires_in":1800,"refresh_token":"yyy", \\ others }
//
thisLog.info("oauthCallback() - completed restTemplate.postForEntity() -- response = " + response);
ObjectMapper mapper = new ObjectMapper();
String access_token = "";
String refresh_token= "";
try {
JsonNode node = mapper.readTree(response.getBody());
access_token = node.path("access_token").asText();
refresh_token = node.path("refresh_token").asText();
}
catch (Exception theE) {
thisLog.error("oauthCallback() -- Exception=" +theE);
}
// at this point, access_token can be used in any other web service call by adding
// it as a header "Authorization: Bearer $access_token"
// we need to send it back to the client so the client can re-submit it on subsequent
// requests to maintain session state at the client rather than in a cache in this servlet
thisLog.info("oauthCallback() - completed restTemplate.postForEntity() " );
Cookie accessJwtCookie = new Cookie("access_token",access_token);
Cookie refreshJwtCookie = new Cookie("refresh_token",refresh_token);
// in real implementations, these calls should be made to ensure communications is limited to HTTPS
// accessJwtCookie.setSecure(true);
// refreshJwtCookie.setSecure(true);
// these restrict the browser's ability to access the cookies to sending HTTP out, blocking script access
accessJwtCookie.setHttpOnly(true);
refreshJwtCookie.setHttpOnly(true);
// these allow browser to send back the cookie for any subsequent URLs on the site
accessJwtCookie.setPath("/");
refreshJwtCookie.setPath("/");
// these limit the retention of the cookie -- access are only good for 300 seconds, refresh for 1800
// so no point in the browser keeping them longer than that
accessJwtCookie.setMaxAge(300);
refreshJwtCookie.setMaxAge(1800);
servletResponse.addCookie(accessJwtCookie);
servletResponse.addCookie(refreshJwtCookie);
thisLog.info("oauthCallback() - attempting redirect to authenticated mainpage - servletResonse=" + servletResponse.toString());
// create a session and use that to create a JSESSION cookie in the response
HttpSession session = servletRequest.getSession(true);
session.setMaxInactiveInterval(5*60); // set to 5 minute idle timeout
// for debuggging, use the refresh_token to test our refresh2Provider() logic
//ClientRegistration testClient = refresh2Provider(refresh_token);
model.addAttribute("diagnostics", response);
model.addAttribute("exception", "(none)");
model.addAttribute("stacktrace","(none)");
// NOTE! -- this redirect is "absolute relative" to the servlet context of /depends/gui
// "redirect:clients/mainpage.html" ---> /depends/gui/access/oauth2/callback/clients/mainpage (WRONG)
// "redirect:/clients/mainpage.html" --> /depends/gui/clients/mainpage
return "redirect:/clients/mainpage";
}
Here is the entire SecurityConfiguration class.
package com.myfirm.dependsgui;
//imports omitted for brevity
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName());
String JWKSETURI = "http://localhost:8011/realms/myfirm/protocol/openid-connect/certs";
//------------------------------------------------------------------------------------
// webClient() - defines a bean that will map configured Authorizaton Server parameters
// from application.properties to a Http client that can call those endpoints to
// verify tokens, etc
//------------------------------------------------------------------------------------
#Bean
WebClient webClient(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
thisLog.debug("webClient() - instantiating new WebClient for this app to interact with each defined Authorization Server");
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository,
authorizedClientRepository);
oauth2.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
}
//----------------------------------------------------------------------------------
// authorizationRequestRepository() - defines bean used by the auto-generated
// login handling to bounce an authorization request over to the
// Authorization Server selected by the interactive user
//----------------------------------------------------------------------------------
#Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest>
authorizationRequestRepository() {
thisLog.debug("authorizedRequestRepository() - instantiating new HttpSessionOAuth2AuthorizationRequestRepository()");
return new HttpSessionOAuth2AuthorizationRequestRepository();
}
//----------------------------------------------------------------------------------
// accessTokenResponseClient() - this mirrors the default function created by the
// OAuth2 libraries for accepting access tokens sent back from an Authorization
// Server. This could be overriden / enhanced if additional info needs to be
// extracted from somewhere after successful authentication to stuff into the JWT
//---------------------------------------------------------------------------------
#Bean
public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
accessTokenResponseClient() {
thisLog.debug("accessTokenResponseClient() - instantiating default NimbusAuthorizationCodeTokenResponseClient() for post-processing of new access tokens");
return new NimbusAuthorizationCodeTokenResponseClient();
}
// -------------------------------------------------------------------------------
// Declare bean oauthPrincipalExtractor() that returns an instance of our
// customized OauthPrincipalExtractor class to extract the desired value of an
// Oauth reply from Keycloak we want used as principal in a Spring Authorization
// -------------------------------------------------------------------------------
#Bean
public PrincipalExtractor keycloakPrincipalExtractor() {
thisLog.debug("keycloakPrincipalExtractor() - instantiating bean of custom KeycloakPrincipalExtractor");
return new KeycloakPrincipalExtractor();
}
// -------------------------------------------------------------------------------
// Declare bean oauthAuthoritiesExtractor() that returns an instance of our
// customized KeycloakAuthoritiesExtractor() class to extract grants from Oauth
// tokens into Spring Security Authentication objects
// -------------------------------------------------------------------------------
#Bean
public AuthoritiesExtractor keycloakAuthoritiesExtractor() {
thisLog.debug("keycloakAuthoritiesExtractor() -- instantiating bean of custom KeycloakAuthoritiesExtractor");
return new KeycloakAuthoritiesExtractor();
}
// -------------------------------------------------------------------------------
// Declare bean dependsAuthenticationSuccessHandler() that returns an instance of our
// customized DependsAuthenticationSuccessHandler() class to perform post-processing
// after successful authentication
// -------------------------------------------------------------------------------
public AuthenticationSuccessHandler dependsAuthenticationSuccessHandler() {
thisLog.debug("dependsAuthenticationSuccessHandler() -- instantiating bean of custom DependsAuthenticationSuccessHandler");
return new DependsAuthenticationSuccessHandler();
}
// -------------------------------------------------------------------------------
// Declare bean dependsAuthenticationFailurHandler() that returns an instance of our
// customized DependsAuthenticationFailureHandler() class to perform post-processing
// after successful authentication
// -------------------------------------------------------------------------------
public AuthenticationFailureHandler dependsAuthenticationFailureHandler() {
thisLog.debug("dependsAuthenticationFailureHandler() -- instantiating bean of custom DependsAuthenticationFailureHandler");
return new DependsAuthenticationFailureHandler();
}
// ----------------------------------------------------------------------------
// keycloakJwtAuthenticationConverter() - defines a mapping that will be used
// by token processing to map claims at the token level to authorities in the
// Spring Security layer for the app
//-----------------------------------------------------------------------------
private JwtAuthenticationConverter keycloakJwtAuthenticationConverter() {
thisLog.debug("keycloakJwtAuthenticationConverter() -- instantiating critiera for grant converter within JwtAuthenticationConverter()");
JwtGrantedAuthoritiesConverter thisgrantauthconverter = new JwtGrantedAuthoritiesConverter();
// the roles we want to extract are under "realm-access": { roles": [ xx,yy,zz] }
thisgrantauthconverter.setAuthoritiesClaimName("realm-access");
thisgrantauthconverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter thisauthconverter = new JwtAuthenticationConverter();
thisauthconverter.setJwtGrantedAuthoritiesConverter(thisgrantauthconverter);
return thisauthconverter;
}
//-----------------------------------------------------------------------------------
// configure(HttpSecurity) - key method for setting filters and OAuth2 parameters
//-----------------------------------------------------------------------------------
#Override
protected void configure(HttpSecurity http) throws Exception {
//------------------------------------------------------------------------------------------------
// NOTE: These patterns are APPENDED to the servlet context /depends/gui in application.properties
//------------------------------------------------------------------------------------------------
thisLog.info("configure(HttpSecurity) - defining access filters for application URI patterns");
// NOTE: using authorizeHttpRequests() instead of older authorizeRequests() -- many online examples
// have not reflected this new directional implmentation - older call is being deprecated
http.authorizeHttpRequests()
.antMatchers(HttpMethod.GET, "/public/**").permitAll()
.antMatchers(HttpMethod.GET, "/css/**").permitAll()
.antMatchers(HttpMethod.GET, "/js/**").permitAll()
.antMatchers(HttpMethod.GET, "/access/**").permitAll()
.antMatchers(HttpMethod.GET, "/clients/**").hasAnyAuthority("SCOPE_myfirmuser","SCOPE_myfirmadmin")
.antMatchers(HttpMethod.GET, "/admin/**").hasAuthority("SCOPE_myfirmadmin")
.anyRequest().authenticated()
.and() // return to the parent http object
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(keycloakJwtAuthenticationConverter())
;
http.formLogin()
.loginPage("/access/oauthproviders")
.successHandler(dependsAuthenticationSuccessHandler())
.failureHandler(dependsAuthenticationFailureHandler())
.and() // return to the parent http object
.oauth2Login()
.loginPage("/access/oauthproviders")
.authorizationEndpoint()
.authorizationRequestRepository(authorizationRequestRepository())
;
}
//-----------------------------------------------------------------------------
// jwtDecoder() - instantiates a JWT decoder using the JWKsetUri of an OAuth
// provider to fetch strings to decode / unencrypt a token
// NOTE -- not clear how this approach works when a single app can use
// multiple OAuth providers for google, facebook, github, keycloak, etc
// For now, this is hardcoding the JwkSetUri for a local keycloak instance.
//-----------------------------------------------------------------------------
#Bean
public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) {
thisLog.info("jwtDecoder() - returning link to method for decoding / validating JWT via Nimbus library");
thisLog.info("jwtDecoder() - incoming properties = " + properties.getJwt().getJwkSetUri());
NimbusJwtDecoder thisDecoder = NimbusJwtDecoder.withJwkSetUri(JWKSETURI).build();
return thisDecoder;
}
} // end of entire class
Here are logs at startup showing the classes referenced in the configure() method ARE getting loaded:
2022-05-12 23:42:45,163 INFO com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - configure(HttpSecurity) - defining access filters for application URI patterns
2022-05-12 23:42:45,171 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - keycloakJwtAuthenticationConverter() -- instantiating critiera for grant converter within JwtAuthenticationConverter()
2022-05-12 23:42:45,178 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - dependsAuthenticationSuccessHandler() -- instantiating bean of custom DependsAuthenticationSuccessHandler
2022-05-12 23:42:45,179 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - dependsAuthenticationFailureHandler() -- instantiating bean of custom DependsAuthenticationFailureHandler
2022-05-12 23:42:45,209 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - authorizedRequestRepository() - instantiating new HttpSessionOAuth2AuthorizationRequestRepository()
2022-05-12 23:42:45,225 INFO com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - jwtDecoder() - returning link to method for decoding / validating JWT via Nimbus library
2022-05-12 23:42:45,225 INFO com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - jwtDecoder() - incoming properties = null
2022-05-12 23:42:45,381 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - webClient() - instantiating new WebClient for this app to interact with each defined Authorization Server
2022-05-12 23:42:45,586 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - accessTokenResponseClient() - instantiating default NimbusAuthorizationCodeTokenResponseClient() for post-processing of new access tokens
2022-05-12 23:42:45,591 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - keycloakPrincipalExtractor() - instantiating bean of custom KeycloakPrincipalExtractor
2022-05-12 23:42:45,591 DEBUG com.myfirm.dependsgui.SecurityConfiguration$$EnhancerBySpringCGLIB$$df1fd8f6 - keycloakAuthoritiesExtractor() -- instantiating bean of custom KeycloakAuthoritiesExtractor

Migrating away from Spring Security OAuth 2

I'm having a Spring Boot Auth Microservice. It uses the Oauth2 spring cloud starter dependency which is deprecated nowadays.
buildscript {
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.1.9.RELEASE"
}
}
dependencies {
implementation "org.springframework.boot:spring-boot-starter-actuator"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.cloud:spring-cloud-starter-oauth2:2.1.5.RELEASE"
}
The Schema was taken from here: https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
It also has a custom user_details table. The JPA class is implementing UserDetails. I've also provided an implementation for UserDetailsService which looks up the user in my custom table.
OAuth Configuration is quite forward:
AuthorizationServerConfiguration - where oauth is configured:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableAuthorizationServer
class AuthorizationServerConfiguration : AuthorizationServerConfigurerAdapter() {
#Autowired private lateinit var authenticationManager: AuthenticationManager
#Autowired private lateinit var dataSource: DataSource
#Autowired
#Qualifier("customUserDetailsService")
internal lateinit var userDetailsService: UserDetailsService
#Autowired
private lateinit var passwordEncoder: BCryptPasswordEncoder
override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) {
endpoints
.tokenStore(JdbcTokenStore(dataSource))
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
}
override fun configure(clients: ClientDetailsServiceConfigurer) {
// This one is used in conjunction with oauth_client_details. So like there's one app client and a few backend clients.
clients.jdbc(dataSource)
}
override fun configure(oauthServer: AuthorizationServerSecurityConfigurer) {
oauthServer.passwordEncoder(passwordEncoder)
}
}
WebSecurityConfiguration - needed for class above:
#Configuration
class WebSecurityConfiguration : WebSecurityConfigurerAdapter() {
#Bean // We need this as a Bean. Otherwise the entire OAuth service won't work.
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}
override fun configure(http: HttpSecurity) {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
}
ResourceServerConfiguration - to configure access for endpoints:
#Configuration
#EnableResourceServer
class ResourceServerConfiguration : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().cors().disable().csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/token").authenticated()
.antMatchers("/oauth/user/**").authenticated()
.antMatchers("/oauth/custom_end_points/**").hasAuthority("my-authority")
// Deny everything else.
.anyRequest().denyAll()
}
}
These few lines give me a lot.
User Info endpoint (used by microservices)
Client's such as Mobile frontends can authenticate using: POST oauth/token and providing a grant_type=password together with a username and a password.
Servers can authorize using 'oauth/authorize'
Basic Auth support with different authorities is also available as I can fill username + password into the oauth_client_details table:
select client_id, access_token_validity, authorities, authorized_grant_types, refresh_token_validity, scope from oauth_client_details;
client_id | access_token_validity | authorities | authorized_grant_types | refresh_token_validity | scope
-------------------+-----------------------+-------------------------------+-------------------------------------------+------------------------+---------
backend | 864000 | mail,push,app-register | mail,push,client_credentials | 864000 | backend
app | 864000 | grant | client_credentials,password,refresh_token | 0 | app
This is used by the app if there's no oauth token yet.
Other microservices also use this to protect their endpoints - such as in this example:
#Configuration #EnableResourceServer class ResourceServerConfig : ResourceServerConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.authorizeRequests()
// Coach.
.antMatchers("/api/my-api/**").hasRole("my-role")
.antMatchers("/registration/**").hasAuthority("my-authority")
}
}
Their set up is quite easy:
security.oauth2.client.accessTokenUri=http://localhost:20200/oauth/token
security.oauth2.client.userAuthorizationUri=http://localhost:20200/oauth/authorize
security.oauth2.resource.userInfoUri=http://localhost:20200/oauth/user/me
security.oauth2.client.clientId=coach_client
security.oauth2.client.clientSecret=coach_client
The first three properties just go to my authorization server. The last two properties are the actual username + password that I've also inserted inside the oauth_client_details table. When my microservice wants to talk to another microservice it uses:
val details = ClientCredentialsResourceDetails()
details.clientId = "" // Values from the properties file.
details.clientSecret = "" // Values from the properties file.
details.accessTokenUri = "" // Values from the properties file.
val template = OAuth2RestTemplate(details)
template.exchange(...)
Now my question is - how can I get all of this with the built in Support from Spring Security using Spring Boot? I'd like to migrate away from the deprecated packages and retain all tokens so that users are still logged in afterwards.
We are also running a spring security authorization server and looked into this. Right now there is no replacement for the authorization server component in spring and there does not seem to be a timeline to implement one. Your best option would be to look into an existing auth component like keycloak or nimbus. alternatively there are hosted service like okta or auth0.
Keeping your existing tokens will be a bit of a challange as you would need to import them into your new solution. Our current tokens are opaque while newer auth-solutions tend to use some version of jwt, so depending on your tokens, keeping them may not even be an option.
Right now we consider accepting both old and new tokens for a time until the livetime of our old tokens ends, at wich point we would move fully to the new infrastukture.
So I've ended up developing my own authentication system with a migration API from the old Spring Security OAuth 2 to my system. That way you are not logged out and need to re-login.
I'll describe how I did it in case anyone is interested.
In my scenario it is 2 'microservices'. One being the deprecated auth and the other leveraging it.
Legacy Authentication System
To either get a token as a user you'd send a request to /oauth/token with your username + password.
To refresh a token another request to /oauth/token with your refresh token.
Both cases return your access token + refresh token. You can execute this multiple times per devices and you'd always end up with the same tokens. This is important later.
Tokens are stored as MD5 hashed.
Spring Security OAuth has these tables defined:
oauth_access_token (access tokens)
oauth_approvals (don't know what for, is always empty in my case)
oauth_client_details (contains a basic authorization method when you're not authorized)
oauth_client_token (empty in my case)
oauth_code (empty in my case)
oauth_refresh_token (refresh tokens)
user_details (contains the user data)
user_details_user_role (association between user + roles)
user_role (your roles)
I really didn't use the multi roles functionality, but in any case it's trivial to take that into consideration as well.
New Authentication System
Access token & refresh tokens are uuid4's that I SHA256 into my table.
I can query them easily and check for expiration and throw appropriate HTTP status codes.
I ended up doing a per device (it's just a UUID generated once in the frontend) system. That way I can distinguish when a user has multiple devices (AFAIK, this isn't possible with the old system).
We need these new endpoints
Login with email + password to get an authentication
Migration call from the old tokens to your new ones
Logout call which deletes your authentication
Refresh access token call
Thoughts
I can keep using the user_details table since only my code interacted with it and I expose it via Springs UserDetailsService.
I'll create a new authentication table that has a n:1 relationship to user_details where I store a device id, access token, access token expiry & refresh token per user.
To migrate from the old to the new system, my frontend will send a one time migration request, where I check for the given access token if it's valid and if it is, I generate new tokens in my system.
I'll handle both systems in parallel by distinguishing at the header level Authorization: Bearer ... for the old system & Authorization: Token ... for the new system
Code snippets
I use Kotlin, so in order to have type safety and not accidentally mix up my old / new token I ended up using a sealed inline classes:
sealed interface AccessToken
/** The token from the old mechanism. */
#JvmInline value class BearerAccessToken(val hashed: String) : AccessToken
/** The token from the new mechanism. */
#JvmInline value class TokenAccessToken(val hashed: String) : AccessToken
To get my token from an Authorization header String:
private fun getAccessToken(authorization: String?, language: Language) = when {
authorization?.startsWith("Bearer ") == true -> BearerAccessToken(hashed = hashTokenOld(authorization.removePrefix("Bearer ")))
authorization?.startsWith("Token ") == true -> TokenAccessToken(hashed = hashTokenNew(authorization.removePrefix("Token ")))
else -> throw BackendException(Status.UNAUTHORIZED, language.errorUnauthorized())
}
internal fun hashTokenOld(token: String) = MessageDigest.getInstance("MD5").digest(token.toByteArray(Charsets.UTF_8)).hex()
internal fun hashTokenNew(token: String) = MessageDigest.getInstance("SHA-256").digest(token.toByteArray(Charsets.UTF_8)).hex()
Verifying the tokens with type safety gets pretty easy:
when (accessToken) {
is BearerAccessToken -> validateViaDeprecatedAuthServer(role)
is TokenAccessToken -> {
// Query your table for the given accessToken = accessToken.hashed
// Ensure it's still valid and exists. Otherwise throw appropriate Status Code like Unauthorized.
// From your authentication table you can then also get the user id and work with your current user & return it from this method.
}
}
The validateViaDeprecatedAuthServer is using the old authentication sytem via the Spring APIs and returns the user id:
fun validateViaDeprecatedAuthServer(): String {
val principal = SecurityContextHolder.getContext().authentication as OAuth2Authentication
requireElseUnauthorized(principal.authorities.map { it.authority }.contains("YOUR_ROLE_NAME"))
return (principal.principal as Map<*, *>)["id"] as? String ?: throw IllegalArgumentException("Cant find id in principal")
}
Now we can verify if a given access token from a frontend is valid. The endpoint which generates a new token from the old one is also quite simple:
fun migrateAuthentication(accessToken: AccessToken) when (origin.accessToken(language)) {
is BearerAccessToken -> {
val userId = validateViaDeprecatedAuthServer(role)
// Now, create that new authentication in your new system and return it.
createAuthenticationFor()
}
is TokenAccessToken -> error("You're already migrated")
}
Creating authentication in your new system might look like this:
fun createAuthenticationFor() {
val refreshToken = UUID.randomUUID().toString()
val accessToken = UUID.randomUUID().toString()
// SHA256 both of them and save them into your table.
return refreshToken to accessToken
}
Then you only need some glue for your new 'login' endpoint where you need to check that the email / password matches a given user in your table, create an authentication & return it.
Logout just deletes the given authentication for your user id + device id.
Afterthoughts
I've been using this system now for the last few days and so far it's working nicely. Users are migrating. No one seems to be logged out which is exactly what I've wanted.
One downside is that since the old authentication system didn't distinguish between devices, I have no way of knowing when a user has successfully migrated. He could be using 1 device or 10. I simply don't know. So both systems will need to live side by side for a rather long time and slowly I'll phase out the old system. In which case, I'll force logout you and you need to re-login (and potentially install a new App version if you haven't updated).
Note that the new system is limited to my own needs, which is exactly what I want. I'd prefer it to be simple and maintainable than the Spring Blackbox authentication system.

Request body is empty when no authentication is present for secure APIs

I am trying to log the request body on all requests in a spring boot reactive application secured with spring security. But I am running into an issue where the request body is logged only if the basic auth header is present (even if the username and password are invalid). But if no auth header is present the request body does not get logged. I am unsure what I am missing and would like to find out how I maybe able to get access to the request body for cases where there is no authentication header present.
The request body logging is done using an authentication entry point set on HttpBasicSpec. The security configuration looks as follows:
#Configuration
#EnableWebFluxSecurity
class SecurityConfiguration {
private val logger = LoggerFactory.getLogger(this::class.java)
#Bean
fun securityConfigurationBean(http: ServerHttpSecurity) =
http.csrf().disable()
.cors().disable()
.httpBasic()
.authenticationEntryPoint { exchange, _ ->
exchange.request.body
.subscribe { logger.info(CharsetUtil.UTF_8.decode(it.asByteBuffer()).toString()) }
.let { Mono.error(HttpServerErrorException(HttpStatus.UNAUTHORIZED)) }
}.and().authorizeExchange().anyExchange().authenticated().and().build()
}
There is a test router config that has a one route:
#Configuration
class TestRouterConfig {
#Bean
fun testRoutes() =
router {
POST("/test") {
ServerResponse.ok().bodyValue("This is a test route")
}
}
}
When I make a request to http:localhost:8080/test with a request body of
{"sample": "sample"}
with an invalid username and password in the basic auth header, I see the following in the console:
2019-12-06 11:51:18.175 INFO 11406 --- [ctor-http-nio-2] uration$$EnhancerBySpringCGLIB$$5b5f0067 : {"sample": "sample"}
But when I remove authentication all together I don't see the above logging statement for the same endpoint (I am using a rest client to make these calls).
The versions of tools/frameworks/languages:
Kotlin: 1.3.50
Spring boot: 2.2.1
Java: 12
Gradle: 5.6.4
Spring dependency management: 1.0.8.RELEASE
I would like to be able to log the request body for all requests that result in an authentication failure including the absence of an authentication header and would appreciate any help in this regard. My apologies if this has been discussed/posted elsewhere.
Thank you!

Springboot, CXF 3.2.7, Swagger2Feature: Authorization header does not appear in the request headers

I'm trying to integrate Swagger 2 into my API, which is implemented with CXF newest version: 3.2.7.
I tried lots of tutorials, the CXF official documentation, others too (e.g. Spring Boot, Apache CXF, Swagger under JAX-RS).
The swagger official website does not help for me. The swagger OpenAPI 2.0 authentication doc is not working, neighter the OpenAPI 3.0.
It is not working with component schemes of Open API 3.0.0, so i stayed with the apiKeyDefinition.
The one, which is working now can be found in this thread, in the answer of #Naoj:
CXF Swagger2Feature adding securityDefinitions
With this solution the swagger ui appeared and also the Autorize button is showing.
I fill the authentication form, and after that, I try to send requests with the swagger-ui. The problem is, that the Authorization header does not appear in the request, so I got 401 response.
In the pom:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-service-description-swagger</artifactId>
<version>3.2.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>3.20.1</version>
</dependency>
My swagger configuration looks like this:
#Configuration
public class SwaggerConfig {
#Bean
#ConfigurationProperties("swagger")
public Swagger2Feature swagger() {
return new ExtendedSwagger2Feature();
}
#Bean
#DependsOn("jaxRsServer")
public ServletContextInitializer initializer() {
return servletContext -> {
BeanConfig scanner = (BeanConfig) ScannerFactory.getScanner();
Swagger swagger = scanner.getSwagger();
servletContext.setAttribute("swagger", swagger);
};
}
}
The extended swagger feature is the following:
#Provider(value = Provider.Type.Feature, scope = Provider.Scope.Server)
public class ExtendedSwagger2Feature extends Swagger2Feature {
#Override
protected void addSwaggerResource(Server server, Bus bus) {
super.addSwaggerResource(server, bus);
BeanConfig config = (BeanConfig) ScannerFactory.getScanner();
Swagger swagger = config.getSwagger();
swagger.securityDefinition("Bearer", new ApiKeyAuthDefinition("authorization", In.HEADER));
}
}
I try to configure Bearer JWT token based authentication. My application.yml contains the following:
swagger:
basePath: /rest
title: Backend Application
description: Swagger documentation of Backend Application REST services
license:
licenceUrl:
contact:
resourcePackage: my.resource.package
scan: true
apiKeyAuthDefinition:
name: Authorization
in: header
type: http
I import the SwaggerConfig into my #SpringBootApplication class like this:
#Import(SwaggerConfig.class)
It is working, as i see, swagger appeared and the title and description field is filled with the properties of my yml.
What am I missing? Any suggestions would be appretiated.
Thanks in advance.
You can simplify your code and remove ExtendedSwagger2Feature and the initializer(). Modify your swagger() method as follows and the output will be the same:
#Bean
#ConfigurationProperties("swagger")
public Swagger2Feature swagger() {
Swagger2Feature swagger2Feature = new Swagger2Feature();
swagger2Feature.setSecurityDefinitions(Collections.singletonMap("bearerAuth",
new ApiKeyAuthDefinition("Authorization", In.HEADER)));
return swagger2Feature;
}
The reason for the token not being added to your request, is that securityDefinitions is just a declaration for the available schemes. You need to apply it to the operation by adding (to your TestResource interface):
#Api( authorizations = #Authorization( value = "bearerAuth" ))
You will notice a lock icon next to the operation in Swagger UI. Currently it's not there.
Btw. you should use the newer OpenApiFeature instead of the old Swagger2Feature. Happy to answer questions, if you have problems with it.

Resources