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

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!

Related

Adding CSRF token in response spring cloud gateway

I have configured CSRF in spring cloud gateway.
I followed the documentation to add the controllerAdvice for adding the CSRF token in the response header.
But it did not work. Below is the code.
#ControllerAdvice
public class SecurityControllerAdvice {
#ModelAttribute
Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
return csrfToken.doOnSuccess(token -> exchange.getAttributes()
.put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token));
}
}
so after some research online I found the below snippet and added it to my GatewayApplication.class.
public WebFilter addCsrfTokenFilter() {
return (exchange, next) -> Mono.just(exchange)
.flatMap(ex -> ex.<Mono<CsrfToken>>getAttribute(CsrfToken.class.getName()))
.doOnNext(ex -> {
})
.then(next.filter(exchange));
}
After this addition, it is adding the token in the response header.
But I am unable to understand this code. Can someone please help me understand this snippet.
Also will this code mutate the request and remove formData. Because i noticed that POST request to my rest apis does not have the formData, it is lost when the request is forwaded to my rest api from the gateway.

Keycloak refuses to return roles on request

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.

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

An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: 406 Not Acceptable

I am trying use spirng-oauth2-client to connect my project with a third-party authentication server (following this instruction), ans right now when I run the application, after the authorization step, I am redirect back for my application, and a page with this error is displayed:
[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: 406 Not Acceptable: [Media is not supported]
In the comments for an answer in another Stack Overflow post, someone suggested that this is happening because "Spring makes the POST for the authenntication code with FORM parameters, whereas mercadolibre expects no body, only query parameters".
I have this configuration right now:
application.properties
spring.security.oauth2.client.registration.mercadolivre.provider=mercadolivre
spring.security.oauth2.client.registration.mercadolivre.client-id=...
spring.security.oauth2.client.registration.mercadolivre.client-secret=...
spring.security.oauth2.client.registration.mercadolivre.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.mercadolivre.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.provider.mercadolivre.authorization-uri=https://auth.mercadolivre.com.br/authorization
spring.security.oauth2.client.provider.mercadolivre.token-uri=https://api.mercadolibre.com/oauth/token
security.java
#Configuration
public class Security extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.defaultSuccessUrl("/");
}
}
Anyone knows how to change the Spring behavior to match th required for the service? I mean, making the POST for the authenntication code with no body, only query parameters?
For me the error was [invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: 401 Unauthorized
The issue was an expired/outdated Client Id and Secret. (I used the Client Id and Secret before and it worked)
This error relates to the response you get from the authentication server, either during client authentication or during fetching of the user-info. We can force the method for both requests to be BASIC instead of POST with these properties
spring.security.oauth2.client.registration.mercadolivre.client-authentication-method=BASIC
spring.security.oauth2.client.provider.mercadolivre.user-info-authentication-method=BASIC
In you controller tha you is redirected for, try to put consumes Json like this:
#GetMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE)
public String indexPage() {
.
.
}
Or MediaType.ALL_VALUE

Spring boot 2 Microservices - Propagate the Principal to services

i'm developing an application with spring boot, spring cloud, and Zuul as gateway.
With Spring Security i created an authorization service which will generate a JWT token when the user login.
The Zuul Gateway can decode this token and let the request go inside my application to the routed services.
The question now is, how can i get the logged user (or the token itself) in one of the microservices? Is there a way to tell the Zuul gateway to attach the token to every request he passes to a routed path?
All the microservices do not have Spring Security as a dependency, because the idea is to check the token only at the gateway level, not everywhere, but i can't find a proper way to do so...
Let's say I have a User Service routed in the gateway. After the login the user wants to check his profile.
He will make a request to {{gateway_url}}/getUser with the token.
The gateway configuration is
zuul:
ignored-services: '*'
sensitive-headers: Cookie,Set-Cookie
routes:
user-service:
path: /user/**
service-id: USER-SERVICE
The gateway will route this request to the USER-SERVICE application, to the getProfile controller method how can i know which is the logged user? Who made the request?
The sensitive headers are a blacklist, and the default is not empty. Consequently, to make Zuul send all headers (except the ignored ones), you must explicitly set it to the empty list. Doing so is necessary if you want to pass cookie or authorization headers to your back end. The following example shows how to use sensitiveHeaders:
application.yml:
zuul:
routes:
users:
path: /myusers/**
sensitiveHeaders:
url: https://downstream
The question now is, how can i get the logged user (or the token
itself) in one of the microservices? Is there a way to tell the Zuul
gateway to attach the token to every request he passes to a routed
path?
You can add any custom header to the request before zuul routes it,
take a look at this code:
#Configuration
public class ZuulCustomFilter extends ZuulFilter {
private static final String ZULL_HEADER_USER_ID = "X-Zuul-UserId";
#Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null && authentication.getPrincipal() != null;
}
#Override
public Object run() throws ZuulException {
if (
SecurityContextHolder.getContext().getAuthentication().getPrincipal() != null &&
SecurityContextHolder.getContext().getAuthentication().getPrincipal() instanceof OnlineUser
) {
OnlineUser onlineUser = (OnlineUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(ZULL_HEADER_USER_ID, onlineUser.getId());
}
return null;
}
}
In this sample, the id of the user is attached to the request and then it is routed to the corresponding service.
This process takes place right after user authorization performed by spring security

Resources