alek
alek

Reputation: 127

How to wrap an OAuth2 exception?

We have a rest API that uses Spring OAuth2. After the user is authenticated, all the JSON responses are in the following format:

{"code" : 12345, "data" : "..." }

But the JSON response for authentication failures is not inline with the above format, as that is handled by Spring.

For example in case of incorrect credentials, the clients get HTTP status code 400 with JSON response as follows:

{"error": "invalid_grant", "error_description": "Bad credentials" }

In case the user account is locked, the clients get HTTP status code 400 with JSON response as follows

{"error":"invalid_grant","error_description":"User account is locked"}

All of this is because Spring TokenEndpoint.handleException() is handling the exceptions associated with /oauth/token

I would like to change the JSON response for OAuth2 failures to follow the first format.

This is what I have tried so far with no success:

  1. Use ControllerAdvice with highest precendence order & use @ExceptionHandler as described here
  2. implementing OAuth2ExceptionRenderer as described here
  3. implement ExceptionMapper
  4. added a new ObjectMapper with extending StdSerializer. Although my objectmapper is initialized it is not being used for serializing the exceptions. Maybe because Spring is calling MappingJackson2HttpMessageConverter directly and there seems to be several instances of this class in my app.

Any help in any of the above approaches or a new one would be highly appreciated.

I haven't tried this approach as I cannot change the contextpath for the existing clients.

Upvotes: 5

Views: 3388

Answers (2)

Guitar
Guitar

Reputation: 21

I faced exact same issue and workout a solution eventually. I use a customized ExceptionHandlerExceptionResolver class as resolver which overwrited method getExceptionHandler as show in below code, and then use @ControllerAdvice with highest precendence order again, finally it works.

public class MyExceptionHandlerExceptionResolver extends ExceptionHandlerExceptionResolver {
private Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = null;

@Override
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
    Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null);
    List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
    if (exceptionHandlerAdviceCache==null){
        exceptionHandlerAdviceCache = new LinkedHashMap<ControllerAdviceBean, ExceptionHandlerMethodResolver>();
        for (ControllerAdviceBean adviceBean:adviceBeans){
            ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
            exceptionHandlerAdviceCache.put(adviceBean, resolver);
        }
    }
    for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
        if (entry.getKey().isApplicableToBeanType(handlerType)) {
            ExceptionHandlerMethodResolver resolver = entry.getValue();
            Method method = resolver.resolveMethod(exception);
            if (method != null) {
                return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
            }
        }
    }
    return null;
}
}

use MyExceptionHandlerExceptionResolver class in configure

@EnableWebMvc
@Configuration
public class WebMVCConfiguration extends WebMvcConfigurationSupport {
@Bean
public ExceptionHandlerExceptionResolver handlerExceptionResolver() {
    MyExceptionHandlerExceptionResolver exceptionResolver = new MyExceptionHandlerExceptionResolver();
    exceptionResolver.setOrder(0);
    exceptionResolver.setMessageConverters(messageConverters());
    return exceptionResolver;
}

private MappingJackson2HttpMessageConverter jsonHttpMessageConverter() {
    return new MappingJackson2HttpMessageConverter();
}

private List<HttpMessageConverter<?>> messageConverters() {
    List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
    messageConverters.add(jsonHttpMessageConverter());
    return messageConverters;
}
}

Upvotes: 1

KSTN
KSTN

Reputation: 2022

If you want to handle the authentication process, you can setup your own custom authentication manager

<oauth:authorization-server
    client-details-service-ref="clientDetails" token-services-ref="tokenServices"
    user-approval-handler-ref="userApprovalHandler">
    <oauth:authorization-code />
    <oauth:implicit />
    <oauth:refresh-token />
    <oauth:client-credentials />
    <oauth:password authentication-manager-ref="customAuthenticationManager" />
</oauth:authorization-server>

<authentication-manager id="customAuthenticationManager"
    xmlns="http://www.springframework.org/schema/security">
    <authentication-provider ref="customAuthenticationProvider" />
</authentication-manager>

<bean id="customAuthenticationProvider"
    class="com.any.CustomAuthenticationProvider">
</bean>

create custom authentication provider that implements AuthenticationProvider

public class UserAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
        String username = auth.getName();
        String password = token.getCredentials().toString();
        User user = userService.loadByUsername(username);
        if(user.isLocked){
            throw new UserLockedException("User is locked");
        }
        if(another.something.bad.happened){
            throw new AnotherSomethingBadHappenedException("Error");
        }

        // setup authorities
        //...

        return new UsernamePasswordAuthenticationToken(user, password, authorities);
    }


}

Now you have your own exception, and by using ExceptionMapper you can translate the exception thrown on authentication process into your custom response message.

Another customization you can create is on Authorization process by creating a custom class that extends ApprovalStoreUserApprovalHandler

public class CustomUserApprovalHandler extends ApprovalStoreUserApprovalHandler {

    // stripped

    @Override
    public AuthorizationRequest checkForPreApproval(AuthorizationRequest authorizationRequest,
            Authentication userAuthentication) {

        ClientDetails client = clientDetailsService
                            .loadClientByClientId(authorizationRequest.getClientId());
        // here, you have the client and the user
        // you can do any checking here and throw any exception
        authorizationRequest.setApproved(approved);
        return authorizationRequest;
    }
}

create bean definition for that class

<bean id="userApprovalHandler"
    class="com.any.CustomUserApprovalHandler">
        <property name="approvalStore" ref="approvalStore" />
        <property name="requestFactory" ref="oAuth2RequestFactory" />
        <property name="clientDetailsService" ref="clientDetails" />
        <property name="useApprovalStore" value="true" />
    </bean>

Upvotes: 1

Related Questions