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());
    }
}