Reputation: 129
I have implemented a simple resource server with Spring Boot using the following dependency.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.4.12</version>
</dependency>
Most of my requirements are satisfied with the built-in, default workflow. However, it would be nice if I could customise the error handling part.
Currently, when I make an unauthorised request, I just get a blank response with a 401 status code.
Instead of an empty response with just a 401 code, I want to send custom error messages like "Invalid access token", "Access token missing", "Access token expired", etc. It would help a lot while debugging.
I have tried implementing the interfaces AuthenticationEntryPoint
and
AuthenticationFailureHandler
. But, I haven't been successful.
I have referred the following article from the Spring Security docs:
https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html
Also, this is my Web Security Configuration:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
}
Can someone provide a way to customise error messages in the JWT Authentication workflow?
Upvotes: 3
Views: 2903
Reputation: 5927
From Spring Security Doc. The error response is handled in th BearerTokenAuthenticationFilter
and the BearerTokenAuthenticationEntryPoint
is responsible to write the response.
You can write your own YourBearerTokenAuthenticationEntryPoint
and add as below in your config.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(resourceServerConfigurer ->
resourceServerConfigurer.authenticationEntryPoint(new YourBearerTokenAuthenticationEntryPoint()))
return http.build();
}
}
PS: This will overwrite the default behavior of BearerTokenAuthenticationEntryPoint
, like write the error message in www-authenticate header based on RFC6750. So, you may copy those behaviour from BearerTokenAuthenticationEntryPoint
to your custom one.
Also, you may want to add your own AccessDeniedHandler
as below
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(resourceServerConfigurer ->
resourceServerConfigurer
.authenticationEntryPoint(new YourBearerTokenAuthenticationEntryPoint()))
.accessDeniedHandler(new YourAccessDeniedHandler()).build();
}
}
Upvotes: 3
Reputation: 11
I stumbled upon the same issue at one of my job tasks and this is a solution I came so far and in your case code might look like the following:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(new
CustomAuthenticationConverter())
.withObjectPostProcessor(new ObjectPostProcessor<BearerTokenAuthenticationFilter>() {
@Override
public <O extends BearerTokenAuthenticationFilter> O postProcess(O filter) {
filter.setAuthenticationFailureHandler((request, response, exception) -> {
BearerTokenAuthenticationEntryPoint delegate = new BearerTokenAuthenticationEntryPoint();
authenticationErrorHandler(response, exception, HttpStatus.UNAUTHORIZED);
delegate.commence(request, response, exception);
});
return filter;
}
})
)
)
.exceptionHandling(exceptionHandler -> exceptionHandler
.accessDeniedHandler((request, response, accessDeniedException) ->
authenticationErrorHandler(response, accessDeniedException, HttpStatus.FORBIDDEN))
.authenticationEntryPoint((request, response, accessDeniedException) ->
authenticationErrorHandler(response, accessDeniedException, HttpStatus.UNAUTHORIZED))
);
return http.build();
}
private void authenticationErrorHandler(HttpServletResponse response, Exception exception, HttpStatus status) throws IOException {
ApiErrorResponse error = new ApiErrorResponse(status.value());
error.setError(status.name());
error.setTimestamp(LocalDateTime.now());
error.addMessage(exception.getMessage());
error.setStackTrace(Arrays.toString(exception.getStackTrace()));
log.error(error.toString());
response.sendError(status.value(), error.getError());
}
withObjectPostProcessor will handle the token validity, it will thrown an error if your token is invalid or expired
The authenticationErrorHandler formats error message
2 separate cases when token is not present or the URL is fobidden is handled in exceptionHandler
In the response body, however the error message will still appear as "unauthorized" due to UNAUTHORIZED code you send for error, but there logging works in order to provide detailed information regarding error.
Upvotes: 1
Reputation: 158
I had the same issue and solved it allowing all to /error/**
. In your case, you can try to change authorizeHttpRequests
part as follows:
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/error/**").permitAll()
.anyRequest().authenticated()
)
Upvotes: 0
Reputation: 1172
If you wish to customize only auth failure response you can do it using the following code
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// your configuration
.exceptionHandling(exceptionHandling -> exceptionHandling
.accessDeniedHandler(new CustomAccessDeniedHandler())
.authenticationEntryPoint(new CustomAuthenticationEntryPoint()))
the above code adds custom exception handlers for authorization and authentication respectively
you should implement CustomAuthenticationEntryPoint
(AuthenticationEntryPoint
) and CustomAccessDeniedHandler
(AccessDeniedHandler
, see AccessDeniedHandlerImpl )
example for implementation of AuthenticationEntryPoint
and return any message that you desire in case of auth failure
@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
log.info("Responding with unauthorized error. Message - {}", e.getMessage());
enter code here
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
e.getLocalizedMessage()); //or replace with response.getWriter() and return anything you desire
}
One thing to keep in mind: it is considered a good practice to return a uniform response and error code for both authentication (401) and authorization (403) failures.
This will make attacker work harder since it would be harder to determine the cause of failure. server log prints should be more informative and specify the exact issue.
Upvotes: 3
Reputation: 417
I have implemented the similar functionality with AuthenticationEntryPoint SpringDoc. Hopefully this is what you are looking.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final RestAuthenticationEntryPoint restAuthEntryPoint;
public SecurityConfig (RestAuthenticationEntryPoint restAuthEntryPoint){
this.restAuthEntryPoint = restAuthEntryPoint;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt).and()
.authenticationEntryPoint(restAuthEntryPoint)
.accessDeniedHandler(accessDeniedHandler()).and()
.sessionManagement(sesionManagement -> sessionManagement.sessionCreationPolicy(sessionCreationPolicy.STATELESS));
return http.build();
}
}
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint{
@Override
public void commerce(HttpServletRequest request, HttpServletRequestResponse response, AuthenticationException e){
// here you can parse error message in return in customse way
Error error = new Error();// Custom model class for you exception
error.setErrors("Token expired", HttpStatus.FORBIDDEN.toString());
response.setStatus(HttpServletRequestResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ServletOutputStream out = response.getOutputStream();
new ObjectMapper().writeValue(out, error);
out.flush();
}
}
Upvotes: 1
Reputation: 11551
Maybe not what you're looking for but the error response is in the headers.
See Bearer Token Failure.
Upvotes: 4