Customize auth error from Spring Security using OAuth2
The accepted answer does not work for me using Oauth2. After some research, the exception translator solution worked.
Basically, you need to create a WebResponseExceptionTranslator
and register it as your exception translator.
First, create a WebResponseExceptionTranslator
bean:
@Slf4j
@Configuration
public class Oauth2ExceptionTranslatorConfiguration {
@Bean
public WebResponseExceptionTranslator oauth2ResponseExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
OAuth2Exception body = responseEntity.getBody();
HttpStatus statusCode = responseEntity.getStatusCode();
body.addAdditionalInformation("timestamp", dateTimeFormat.format(clock.instant()))
body.addAdditionalInformation("status", body.getHttpErrorCode().toString())
body.addAdditionalInformation("message", body.getMessage())
body.addAdditionalInformation("code", body.getOAuth2ErrorCode().toUpperCase())
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
// do something with header or response
return new ResponseEntity<>(body, headers, statusCode);
}
};
}
}
Now you need to change your Oauth2 configuration to register the bean WebResponseExceptionTranslator
:
@Slf4j
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private ClientDetailsServiceBuilder builder;
@Autowired
private WebResponseExceptionTranslator oauth2ResponseExceptionTranslator;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) {
clients.setBuilder(builder);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.exceptionTranslator(oauth2ResponseExceptionTranslator);
}
}
The final result will be:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource",
"code": "UNAUTHORIZED",
"message": "Full authentication is required to access this resource",
"status": "401",
"timestamp": "2018-06-28T23:55:28.86Z"
}
You can see that I did not remove the error
and error_description
from the original body of OAuth2Exception
. I recommend to maintain them because these two fields are following the OAuth2 specification. See the RFC and OAuth2 API definitions for more details.
You can also customize the result: override the error
or error_description
(just calling addAdditionalInformation
), identify a specific exception with instance of
to return a different json result, etc. But there are restriction too: if you want to define some field as integer
, I don't think it's possible, because the addAdditionalInformation
method only accepts String
as type.
I got it :)
https://stackoverflow.com/a/37132751/2520689
I need to create a new class which implements "AuthenticationEntryPoint" as the following:
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint
{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2) throws IOException, ServletException
{
final Map<String, Object> mapBodyException = new HashMap<>() ;
mapBodyException.put("error" , "Error from AuthenticationEntryPoint") ;
mapBodyException.put("message" , "Message from AuthenticationEntryPoint") ;
mapBodyException.put("exception", "My stack trace exception") ;
mapBodyException.put("path" , request.getServletPath()) ;
mapBodyException.put("timestamp", (new Date()).getTime()) ;
response.setContentType("application/json") ;
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED) ;
final ObjectMapper mapper = new ObjectMapper() ;
mapper.writeValue(response.getOutputStream(), mapBodyException) ;
}
}
And add it to my ResourceServerConfigurerAdapter implementation:
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter
{
@Override
public void configure(HttpSecurity http) throws Exception
{
http.exceptionHandling().authenticationEntryPoint(new AuthExceptionEntryPoint()) ;
}
}
You can find my GitHub project which implements everything you need:
https://github.com/pakkk/custom-spring-security
Story short: https://github.com/melardev/JavaSpringBootOAuth2JwtCrudPagination.git
After reading @pakkk response I was not agree, so I decided to try my own thoughs, which also fail, so I decided to take a look at the Spring Security source code itself, what happens is this:
There is a Filter which gets called very very early, the OAuth2AuthenticationProcessingFilter.
This filter tries to extract the JWT from the header, if an exception is thrown it calls
its authenticationEntryPoint.commence() (@pakk was right here)
I have tried to add a Filter to check if it gets called when the Jwt is invalid or present, and it did not, so, adding a custom filter to change the response won't work.
Then I looked where the OAuth2AuthenticationProcessingFilter is configured, and I found out that it is setup on ResourceServerSecurityConfigurer::configure(HttpSecurity http).
With that said, let's see how we can hook into the process.
It turns out to be very easy, since you will be extending the ResourceServerConfigurerAdapter class in your Resource Server application:
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
// ....
}
You go ahead and override:
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
}
As you can see, yes! you have access to ResourceServerSecurityConfigurer, so now what? well let's replace the default entry point by ours:
@Autowired
private AuthenticationEntryPoint oauthEntryPoint;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.authenticationEntryPoint(oauthEntryPoint);
}
For a complete source code with example look at: https://github.com/melardev/JavaSpringBootOAuth2JwtCrudPagination.git
Without this steps, at least for me it wouldn't work, the response provided by @pakkk does not work for me, I checked on the debugger, and by default the entry point used is not ours, even using:
http.and().exceptionHandling().authenticationEntryPoint(oauthEntryPoint)
which was the first thing I tested, to make it work you have to change the entry point directly from the ResourceServerSecurityConfigurer class.
And this is my entrypoint: notice I am sending the ErrorResponse object which is my own class, so I have full control over the response:
@Component
public class OAuthEntryPoint implements AuthenticationEntryPoint {
@Autowired
ObjectMapper mapper;
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ServletServerHttpResponse res = new ServletServerHttpResponse(httpServletResponse);
res.setStatusCode(HttpStatus.FORBIDDEN);
res.getServletResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
res.getBody().write(mapper.writeValueAsString(new ErrorResponse("You must authenticated")).getBytes());
}
}