Customize auth error from Spring Security using OAuth2

10,201

Solution 1

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

Solution 2

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.

Solution 3

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());
    }
}
Share:
10,201

Related videos on Youtube

pakkk
Author by

pakkk

Updated on September 15, 2022

Comments

  • pakkk
    pakkk over 1 year

    I was wondering if I could customize the following authorization error:

    {
      "error": "unauthorized",
      "error_description": "Full authentication is required to access this resource"
    }
    

    I get it when the user request does not have permissions. And I would like to customize it to be quite similar than Spring Boot error:

    {
     "timestamp":1445441285803,
     "status":401,
     "error":"Unauthorized",
     "message":"Bad credentials",
     "path":"/oauth/token"
    }
    

    Could it be possible?

    Many thanks.

  • pakkk
    pakkk over 6 years
    This error type is never caught by the previous @ControllerAdvice :(
  • Aderemi Dayo
    Aderemi Dayo almost 5 years
    Goddam it, where is the OAuthException itself?
  • Melardev
    Melardev almost 5 years
    No man, the AuthenticationEntryPoint is used when credentials are not provided, indeed the commence method is self explanatory, it states how you are gonna tell the user he should START authentication, which is different that letting him know the credentials are invalid. Anyways, go ahead an provide invalid credentials and you will see your entry point does not get triggered.
  • Melardev
    Melardev almost 5 years
    Well, I have reviewed the Source code and I was wrong, but I still don't agree with you(I have not tested your code though, but I have in my app with similar setup). Please read my response, I have provided greater details.
  • Korashen
    Korashen over 4 years
    ControllerAdvice will not work, as the authentication is processed before the controllers (incl. ControllerAdvice) will be invoked.
  • Dherik
    Dherik over 4 years
    @SledgeHammer This works perfect on the project that I used this solution. Verify your Spring version, I used some 1.5.X version. Do you register the exception translator? The translator is directly registered on the Spring configuration, it's not part of my solution.
  • Taher
    Taher over 4 years
    Works like a charm for spring boot 2.1.9 release
  • aswzen
    aswzen over 3 years
    How to remove existing error_description and error ??
  • Dherik
    Dherik over 3 years
    @aswzen I can't say exactly how, but you can start trying to create a new instance of OAuth2Exception or any other class that use the same implementation.
  • aswzen
    aswzen over 3 years
    already tried to extend the OAuth2Exception but got no expected results, what if i create a new question in SO then u answer it? ;)
  • Dherik
    Dherik over 3 years
    @aswzen I really can't remember anymore :(
  • Jodee
    Jodee about 3 years
    For anyone wondering how to remove/replace "error" an "error_description" here is an article describing it (Based on the WebResponseExceptionTranslator) medium.com/@beladiyahardik7/…
  • wonhee
    wonhee over 2 years
    FYI This solution based on DefaultWebResponseExceptionTranslator shouldn't be used or recommended as the class is deprecated as part of Spring Security 5.2.