Reputation: 127
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:
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
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
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