Spring Security: Handle Unauthorized and Unauthenticated requests
An implementation guideline to handle auth failures for REST services
While there are several tutorials covering integration of spring security in spring applications. This tutorial covers on how to handle response of only the two main aspects;
- Unauthorized: You have a valid credential (e.x. JWT) but don’t have access to the certain resources (e.x. Ones that are configured with hasRole(‘blahblah’) // not allowed URLs in security config mentioned below) (Http Status code: 403)
- Unauthenticated: Your have an invalid login credential (Http Status code: 401)
The example code is based on Spring webflux security but is similar to normal spring boot (without the Mono class part).
Note: Global custom handlers wont cover handle this for you. References on security exceptions hierarchy, if you like to dig in (https://docs.spring.io/spring-security/reference/servlet/architecture.html)
You can follow detailed tutorial on https://medium.com/zero-equals-false/protect-rest-apis-with-spring-security-and-jwt-5fbc90305cc5 for implementing the security in your spring boot.
Show me the code…
Your typical wired spring security configuration
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
@Configuration
@EnableWebFluxSecurity //webflux related
@EnableReactiveMethodSecurity //webflux related
public class SecurityConfig {
//whitlisting swagger urls
private static final String[] AUTH_WHITELIST = {
// -- swagger ui
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
"/v3/api-docs/**"};
//custom auth classes for validating JWT
@Autowired
private AuthenticationManager authenticationManager;
//custom auth classes for validating JWT
@Autowired
private SecurityContextRepository securityContextRepository;
@Bean
SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
http.csrf()
.disable()
//Disable Sessions
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
// handlers fro 401 and 403
.exceptionHandling(exception -> exception.accessDeniedHandler(new CustomAccessDeniedHandler())
.authenticationEntryPoint(new CustomAuthenticationEntryPoint()))
//rest services don't have a login form
.formLogin()
.disable()
//disabled basic authentication
.httpBasic()
.disable()
.authorizeExchange()
//used when connecting from browsers
.pathMatchers(HttpMethod.OPTIONS)
.permitAll()
//other whitelist URL, like swagger. ensure to disable in production
.pathMatchers(AUTH_WHITELIST)
.permitAll()
//your auth URL
.pathMatchers("/auth/login")
.permitAll()
.and()
//spring authentication manager
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.anyExchange()
.authenticated()
.and();
return http.build();
}
}
Next, Authentication failure Handler
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class CustomAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
//Authentication entry point has commence method when failures occur
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return new AuthFailureHandler().formatResponse(response);
}
}
Next, Authorization failure handler
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class CustomAccessDeniedHandler implements ServerAccessDeniedHandler {
//Access Denied / unauthorized has handle method when failures occur
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException accessDeniedException) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
return new AuthFailureHandler().formatResponse(response);
}
}
Next generic response section to format response when these two cases occur.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.godwinpinto.authable.application.rest.auth.json.ApiResponse;
import lombok.NoArgsConstructor;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
@NoArgsConstructor
public class AuthFailureHandler {
public Mono<Void> formatResponse(ServerHttpResponse response) {
response.getHeaders()
.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
ObjectMapper mapper = new ObjectMapper();
ApiResponse apiResponse = new ApiResponse(response.getStatusCode()
.value(), "Access Denied", null);
StringBuilder json = new StringBuilder();
try {
json.append(mapper.writeValueAsString(apiResponse));
} catch (JsonProcessingException jsonProcessingException) {
}
String responseBody = json.toString();
byte[] bytes = responseBody.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory()
.wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
}
Hope this clears how to handle both cases (authentication & authorization failures). :)
Hit Clap, if the article helped.