How to set custom 403 response in OAuth2 resource server? - spring-security

Spring boot 2.4.3
If the user has no permission for action, it gives 403 response but without any response body. However the header WWW-Authenticate is set:
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token."
I would like still to have some response body with message. How could I achieve it?
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.formLogin(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.authorizeRequests(authorize -> authorize
.mvcMatchers(HttpMethod.GET, "/app/**", "/admin/**").authenticated()
.mvcMatchers(HttpMethod.PUT, "/app/**", "/admin/**").authenticated()
.mvcMatchers(HttpMethod.POST, "/app/**", "/admin/**").authenticated()
)
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}

I had the same problem and found the following solution:
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
e.getCause().printStackTrace();
response.setStatus(...);
response.setContentType(...);
response.getWriter().write("your custom error response");
}
}
Then in your security config:
http.
//...
.oauth2ResourceServer()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())

Related

Spring OAuth2.0 : Spring Authorization Server 1.0 and Resource server in the same boot application

I'm struggling to use the very same Spring Boot 3.0 application as both authentication server and resource server, but until now, I've not been able to make the whole thing working.
First, I defined a very simple RestController:
#RestController
#RequestMapping("api")
public class PublicAPI {
#GetMapping("/apitest")
public String test(Principal principal) {
return " This is a test ==>";
}
}
Then, essentially following the code found in a Sample project of Spring, I managed to setup my boot app as Spring Authorization Server. I'm able to use Postman to get the authentication token using Oauth2 flow: I'm redirected to Spring's standard login page, I log in with credentials, and I get the Token.
Problem is, if I try to GET http://localhost:9000/api/apitest` using provided token, I get a 401 response from Spring boot.
This is my Security Configuration:
#Bean
#Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, CorsConfiguration configCors) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
http
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
);
http.cors().configurationSource(request -> configCors);
return http.build();
}
#Bean
#Order(2)
SecurityFilterChain apiFilter(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests()
.requestMatchers("/api/**").authenticated()
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
#Bean
#Order(3)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, CorsConfiguration configCors) throws Exception {
http
.securityMatcher("/oauth2/**", "/login")
.authorizeHttpRequests()
.requestMatchers("/login", "/oauth2/**")
.authenticated()
.and()
.formLogin(Customizer.withDefaults());
http.cors().configurationSource(request -> configCors);
return http.build();
}
#Bean
public CorsConfiguration corsConfiguration() throws Exception {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(List.of("*"));
configuration.setAllowedHeaders(List.of("*"));
return configuration;
}
If I try to access another Spring API in a different Spring Boot application which uses the first one as Authentication Server I get no errors.
Pretty sure that there's something wrong my configuration... any hint will be greatly appreciated !
At the very end, it turned out that another filter has been configured:
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class LoopbackIpRedirectFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getServerName().equals("localhost") && request.getHeader("host") != null) {
UriComponents uri = UriComponentsBuilder.fromHttpRequest(new ServletServerHttpRequest(request))
.host("127.0.0.1").build();
response.sendRedirect(uri.toUriString());
return;
}
filterChain.doFilter(request, response);
}
}
Removing the LoopbackIpRedirectFilter problem was fixed

Spring Security protect routes hasRole, hasAuthority do not work with CORS http request

I'm using Spring Security JWT to secure Angular login page.
I configure Spring CORS filter to accept http CORS request.
I verify the JWT is valid in return and authenticated with correct roles.
The secure routes such as hasRole, hasAuthority fail on CORS requests, it always return 403 status.
//http config
protected void configure(HttpSecurity http) throws Exception{
http.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/list_customers").hasAnyRole("ADMIN","DEV") //FAIL 403 error
.antMatchers(HttpMethod.GET, "/api/list_customers").hasAuthority("ROLE_ADMIN") //FAIL 403 error
.anyRequest().authenticated()
}
//CORS filter
public class CORSFilter extends GenericFilterBean implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
HttpServletResponse httpResp = (HttpServletResponse) response;
httpResp.setHeader("Access-Control-Allow-Origin", "*");
httpResp.setHeader("Access-Control-Allow-Methods", "*");
httpResp.setHeader("Access-Control-Allow-Headers", "*");
httpResp.setHeader("Access-Control-Allow-Credentials", "true");
httpResp.setHeader("Access-Control-Max-Age", "3600");
chain.doFilter(request, response);
}
}
Delete your filter and replace it with
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600l);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
Reference
Have a loot at org.springframework.web.filter.CorsFilter to see how it picks up CorsConfigurationSource bean and configures the cors filter
As you can see in the chain of filters, CorsFilter is earlier in the chain than filters checking if the user is authenticated or if they have right permission. Your 403 is thrown much earlier by the CorsFilter as it is not configured correctly
Issue Resolved.
I found out my production role name is different.
I changed the role name from ADMIN to ROLE_ADMIN, then it works fine.

Remove HTTP Strict Transport Security (HSTS) response header in spring oauth2 token API

I am using Spring Security and Spring Oauth2 and JWT in my API project
The default API in order to login which Spring oauth 2 provided, is /oauth/token
This API always adds "Strict-Transport-Security: max-age=31536000 ; includeSubDomains" header to the response.
But I don't want this in my situation. And I have removed HSTS with the below source code.
#EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers()
.httpStrictTransportSecurity().disable();
}
}
With above code, APIs I defined is removed HSTS in header. But the default API /oauth/token still return HSTS in header.
Is there any way to do this ?
Please help.
Thanks,
Tin
I just ran into the same issue.
The best solution I found is writing a filter that prevents others from setting the HSTS header in general.
#Component
#Order(value = Ordered.HIGHEST_PRECEDENCE)
public class HstsHeaderPreventionFilter implements Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, new HttpServletResponseWrapper((HttpServletResponse) response) {
public void setHeader(String name, String value) {
if (!name.equalsIgnoreCase("Strict-Transport-Security")) {
super.setHeader(name, value);
}
}
});
}
#Override
public void destroy() {
}
}

SESSIONID injection for basic authentication requests

I have a Spring-cloud based micro-services application. I also have an API gateway in front of all these services.
I need to support two types of clients.
One of them can call my application using an authorization token (by calling /authorize for example). The token is basically the SESSION ID. All the servers share the session by using Spring Session Redis.
The second client can only send me http basic authentication (user:pass as authorization header).
In the case of the second client, I need to check if the user is already authenticated and has an active session in redis. I added filter before BasicAuthenticationFilter in my security configuration to check that.
If the user has an active session, I'm putting the SESSIONID in the header, and removing the authorization header from the request (I'm using a custom HttpServletRequest wrapper for that). My purpose was that from that point on, Spring will manage the request in the downstream micro-services as if it was sent with a SESSIONID. The reason for that is to avoid a very long login time (more than 1 second).
Here’s my issue: when spring checks if the SESSIONID exists, it checks the original request which doesnt have any sessionId.
Security configuration:
#Resource
#Qualifier("sessions")
private Map<String, String> sessions;
#Autowired
#Qualifier("httpSessionStrategy")
HttpSessionStrategy sessionStrategy;
#Override
protected void configure(HttpSecurity http) throws Exception {
// // #formatter:off
http
.addFilterBefore(setSessionIdInHeader(), BasicAuthenticationFilter.class)
.sessionManagement()
.and()
.exceptionHandling()
.authenticationEntryPoint(restEntryPoint())
.and()
.headers().addHeaderWriter(new StaticHeadersWriter("Server",""))
.and()
.httpBasic()
.authenticationEntryPoint(restEntryPoint())
.and()
.logout().addLogoutHandler(clearTicketOnLogoutHandler())
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.authorizeRequests()
.antMatchers("/index.html", "/login", "/").permitAll()
.antMatchers(HttpMethod.OPTIONS).denyAll()
.antMatchers(HttpMethod.HEAD).denyAll()
.anyRequest().authenticated()
.and()
.authenticationProvider(authenticationProvider)
.csrf()
.disable()
.addFilterAfter(ticketValidationFilter(), SessionManagementFilter.class)
.addFilterAfter(changePasswordFilter(), SessionManagementFilter.class)
.addFilterAfter(httpPolutionFilter(), SessionManagementFilter.class)
.addFilterAfter(saveSessionId(), SessionManagementFilter.class);
// #formatter:on
}
Filter to add header to request:
private Filter setSessionIdInHeader(){
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Jedis jedis = null;
String authorization = request.getHeader("authorization");
String sessionId = null;
if (authorization != null){
if (sessions.get(authorization) != null){ //user already authenticated
sessionId = sessions.get(authorization);
jedis = getJedisPool().getResource();
if (jedis.hgetAll("spring:session:sessions:"+sessionId) != null){ //session alive in redis
log.info("session :"+ sessionId +" exists in redis");
HeaderMapRequestWrapper wrapper = new HeaderMapRequestWrapper(request);
wrapper.addHeader("TOKEN", sessionId);
wrapper.addHeader("mock_authorization", authorization);
filterChain.doFilter(wrapper, response);
}
}
}
filterChain.doFilter(request, response);
}
};
}
Change header name of SESSIONID:
#Bean
public HeaderHttpSessionStrategy httpSessionStrategy(){
HeaderHttpSessionStrategy headerHttpSessionStrategy = new HeaderHttpSessionStrategy();
headerHttpSessionStrategy.setHeaderName("TOKEN");
return headerHttpSessionStrategy;
}
private Filter saveSessionId() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if(request.getHeader("authorization") != null){
sessions.put(request.getHeader("authorization"), request.getSession().getId());
}else{
sessions.put(request.getHeader("mock_authorization"), request.getSession().getId());
}
}
};
}

Zuul Proxy CORS header contains multiple values, headers repeated twice - Java Spring Boot CORS filter config

Why would I be getting every CORS header doubled? I am using the Zuul Proxy to have the request to a service proxied through an API gateway.
I must have something misconfigured with my spring security filtering order.
When I access a route that requires authentication I am getting an error like:
Request to service through API Gateway error
XMLHttpRequest cannot load https://myservice.mydomain.com:8095/service/v1/account/txHistory?acctId=0.
The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed.
Origin 'http://localhost:9000' is therefore not allowed access.
Chrome network log
I checked the response in Chrome devtools and sure enough the CORS headers are repeated twice:
So this looks like somehow my CORS filter is being called twice for each reply. I don't know why that would be happening at this point. It could be that my filter is added before the ChannelProcessingFilter.
Code for API Gateway CORS filter:
public class SimpleCORSFilter implements Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
res.setHeader("Access-Control-Max-Age", "3600");
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, x-requested-with, Cache-Control");
chain.doFilter(request, res);
}
#Override
public void destroy() {}
}
My API Gateway security configuration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Inject
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
private UserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/health","/metrics", "/v1/users/register").permitAll()
.antMatchers("/mappings", "/v1/**", "/service/**").authenticated()
.and()
.httpBasic()
.realmName("apiRealm")
.and()
.csrf()
.disable()
.headers()
.frameOptions().disable()
.and().addFilterBefore(new SimpleCORSFilter(), ChannelProcessingFilter.class);
}
}
I could solve this by checking if the header is null and then setting it only if it is empty or null, though that does not seem like the best solution. I would like to understand what I have done to cause the headers to be preset twice.
I also had the same issue, and i added the CorsFilter into the class where has # EnableZuulProxy, but it still didn't solve my problem.
According to the github Q&A Zuul Access-Control-* Headers are duplicated
zuul.ignored-headers=Access-Control-Allow-Credentials, Access-Control-Allow-Origin
To add it to my zuul's bootstrap.properties, it works!!!
I had a similar problem but the issue was that I had CORS filter in both APIGateway and other services. IF thats not your case then try this CORS filter.
Add this to the class where you have #EnableZuulProxy in the API Gateway. This should do the trick i have a similar configuration on mine.
#Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
For me this solution worked to solve CORS problem in zuul.
endpoints.cors.allowed-origins=*
endpoints.cors.allowed-headers=*
endpoints.cors.allowed-methods=*
However, this does not seem to work for me in one of my staging environment.
I fixed by adding filter into cloud gateway:
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
Thank to https://lifesaver.codes/answer/doubled-cors-headers-after-upgrade-to-greenwich-728

Resources