I have this response when it comes to check the user Unauthorized.
i there any possibility to remove the Path from the Unauthorized response ? since it does not gives valuable information for the user
{
"timestamp": "2021-03-18T09:16:09.699+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/test/v1/api/test.com/config/settings"
}
this is how my config looks like
public class ResourceConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.cors();
httpSecurity
.anonymous().disable()
.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/api/**")
.authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler(new OAuth2AccessDeniedHandler());
}
Adding on #linhx idea of using custom AuthenricationEntryPoint, you can use HandlerExceptionResolver which resolves to a page.
You can get a detailed comparison of different approaches here.
#Component
public class ABAuthenticationEntryPoint implements AuthenticationEntryPoint {
protected final Logger logger = LoggerFactory.getLogger(ABAuthenticationEntryPoint.class);
private final String realmName = "CustomRealm";
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
resolver.resolveException(request, response, null, authException);
}
}
The HandlerExceptionResolver uses the handler (HandlerMethod) to obtain the Controller class and scan it for methods annotated with #ExceptionHandler. If one of this methods matches the exception (ex) then this methods get invoked in order to handle the exception. (else null get returned signaling that this exception resolver feels no responsible).
So, add a class with #ControllerAdvice:
#ExceptionHandler(value = InsufficientAuthenticationException.class)
public ResponseEntity<Object> handleInsufficientAuthenticationException(InsufficientAuthenticationException ex) {
String methodName = "handleInsufficientAuthenticationException()";
return buildResponseEntity(HttpStatus.UNAUTHORIZED, null, null, ex.getMessage(), null);
}
private ResponseEntity<Object> buildResponseEntity(HttpStatus status, HttpHeaders headers, Integer internalCode, String message, List<Object> errors) {
ResponseBase response = new ResponseBase()
.success(false)
.message(message)
.resultCode(internalCode != null ? internalCode : status.value())
.errors(errors != null
? errors.stream().filter(Objects::nonNull).map(Objects::toString).collect(Collectors.toList())
: null);
return new ResponseEntity<>((Object) response, headers, status);
}
SecurityConfig class:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
#Autowired
private ABAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.
.....
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); //AuthenticationEntryPoint has to be the last
}
}
Finally you will get something like the following, based on how you buildResponseEntity
{
"success": false,
"resultCode": 401,
"message": "Full authentication is required to access this resource"
}
Related
We are using spring boot 2.2.2.
I want to return the custom error message in the json response along with 401 status code when user enters the wrong credentials for that I did following steps.
WebSecurityConfig.java:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
public WebSecurityConfig(RepositoryUserDetailsService userDetailsService RestAuthenticationEntryPoint restAuthenticationEntryPoint) {
this.userDetailsService = userDetailsService;
this.restAuthenticationEntryPoint = restAuthenticationEntryPoint;
}
#Override
public void configure(final WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/proj-name/**/*.{js,html}")
.antMatchers("/test/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint)
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/login").permitAll()
.antMatchers("/error").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
}
#Bean
RestAuthenticationEntryPoint authenticationEntryPoint() {
return new RestAuthenticationEntryPoint();
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
#Bean
public CustomAuthenticationProvider customDaoAuthenticationProvider() throws Exception {
CustomAuthenticationProvider customDaoAuthenticationProvider = new CustomAuthenticationProvider();
customDaoAuthenticationProvider.setPasswordEncoder(new PasswordEncoderConfig().encoder());
customDaoAuthenticationProvider.setUserDetailsService(userDetailsService);
return customDaoAuthenticationProvider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customDaoAuthenticationProvider());
}
}
RestAuthenticationEntryPoint.java
#Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.addHeader("WWW-Authenticate", "Basic realm=" + "");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = httpServletResponse.getWriter();
writer.println("HTTP Status 401 - " + e.getMessage());
}
}
CustomAuthenticationProvider.java
public class CustomAuthenticationProvider extends DaoAuthenticationProvider {
#Autowired
private PasswordEncoder passwordEncoder;
#Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "test bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
throw new BadCredentialsException(
messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Invalid credentials"));
}
}
}
When I hit the /login api is returning the following response: Full authentication is required to access this resource
If I add .antMatchers("/error").permitAll() is returning the following response.
{
"timestamp": 1594970514264,
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/login"
}
I tried adding #Component annotation on the CustomAuthenticationProvider then it throws the following error on server startup: java.lang.IllegalArgumentException: A UserDetailsService must be set
I tried adding the CustomAuthenticationFailureHandler by following the url, but didn't work. I've seen couple of articles, but that examples also didn't work. I've also tried creating an exception handler class with #ControllerAdvice annotation still didn't work. Is there any other way to override the default error message or am I missing any annotations or configuration? Thanks in advance for the help!
You could use a controller advice to handle the error thrown by your service :
#ControllerAdvice
#Order(HIGHEST_PRECEDENCE)
public class AppointmentErrorController {
#ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiError> handleError(HttpServletRequest request, BadCredentialsException ex) {
ApiError apiError = new ApiError(...); //retrieve info from BadCredentialsException
return ResponseEntity.status(HttpStatus.UNAUTHORIZED, apiError);
}
I have implemented authentication for my APIs and it works as expected . The user first access the auth api to get a token by passing username and password. This api returns a token. The user then calls the secure apis by passing the tokens.
This issue is when the user passes an invalid token or does not pass a token the default error object is returned from Spring Boot. I wanna customize this object and for this, I wrote a custom exception handler extending ResponseEntityExceptionHandler but this is not getting triggered as the exception is thrown before the controller kicks in.
#ExceptionHandler(value = {InsufficientAuthenticationException.class})
public final ResponseEntity<Object>
authenticationException(InsufficientAuthenticationException ex) {
List<String> details = new ArrayList<>();
details.add("Authentication is required to access this resource");
ErrorResponse error = new ErrorResponse("error", "Unauthorized", details);
return new ResponseEntity(error, HttpStatus.FORBIDDEN);
}
The AuthenticationProvider is responsible to find user based on the authentication token sent by the client in the header. This is how our Spring based token authentication provider looks like:
#Component
public class AuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#Autowired
CustomerService customerService;
#Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
//
}
#Override
protected UserDetails retrieveUser(String userName, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
Object token = usernamePasswordAuthenticationToken.getCredentials();
return Optional
.ofNullable(token)
.map(String::valueOf)
.flatMap(customerService::findByToken)
.orElseThrow(() -> new UsernameNotFoundException("Cannot find user with authentication token=" + token));
}
The token authentication filter is responsible to get the authentication filter from the header and call the authentication manager for authentication. This is how the authentication filter looks like:
public class AuthenticationFilter extends AbstractAuthenticationProcessingFilter {
AuthenticationFilter(final RequestMatcher requiresAuth) {
super(requiresAuth);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
Optional tokenParam = Optional.ofNullable(httpServletRequest.getHeader(AUTHORIZATION)); //Authorization: Bearer TOKEN
String token= httpServletRequest.getHeader(AUTHORIZATION);
token= StringUtils.removeStart(token, "Bearer").trim();
Authentication requestAuthentication = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(requestAuthentication);
}
#Override
protected void successfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain, final Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
Spring security configuration looks like:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PROTECTED_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/api/**")
);
AuthenticationProvider provider;
public SecurityConfiguration(final AuthenticationProvider authenticationProvider) {
super();
this.provider = authenticationProvider;
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(provider);
}
#Override
public void configure(final WebSecurity webSecurity) {
webSecurity.ignoring().antMatchers("/token/**");
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.and()
.authenticationProvider(provider)
.addFilterBefore(authenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.requestMatchers(PROTECTED_URLS)
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
}
#Bean
AuthenticationFilter authenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationManager(authenticationManager());
//filter.setAuthenticationSuccessHandler(successHandler());
return filter;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(HttpStatus.FORBIDDEN);
}
#Autowired
private HandlerExceptionResolver handlerExceptionResolver;
public AuthenticationEntryPoint authenticationEntryPoint() {
log.error("in authenticationEntryPoint");
return new AuthenticationEntryPoint() {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.error("in commence");
try {
log.error(authException.getLocalizedMessage());
handlerExceptionResolver.resolveException(request, response, null, authException);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ServletException(e);
}
}
};
}
}
P.S.: Refer to https://www.javadevjournal.com/spring/securing-a-restful-web-service-with-spring-security/
Since you are customising AbstractAuthenticationProcessingFilter , you can also customise its AuthenticationFailureHandler which will be invoked when attemptAuthentication() throw AuthenticationException. You can then handle the error at there.
An example is :
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler{
#Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
//create your custom error object
CustomError error = xxxxx;
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// Format the custom error object as JSON string , for example using Jackson :
ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(error));
}
}
And configure to use it:
#Bean
AuthenticationFilter authenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
return filter;
}
So, I have created my own authorization server for my application. It also plays the role of a Resource Server, just for testing at the moment.
For consistency I would like to customize the responses for both OAuth2 endpoints (token request fails - authentication error for example), and resource server endpoints (authorization errors). I've seen various resources online which mentioned creating a custom AuthenticationEntryPoint or an ExceptionTranslator but none worked for me.
This is the ApiError model:
public class ApiError {
private List<String> errors = new ArrayList<>();
private LocalDateTime timestamp;
private Cause cause;
private HttpResponseStatus httpResponseStatus;
//constructors, getters, setters
private class HttpResponseStatus {
private int value;
private String message;
public HttpResponseStatus(HttpStatus httpStatus) {
this.value = httpStatus.value();
this.message = httpStatus.getReasonPhrase();
}
public int getValue() {
return value;
}
public String getMessage() {
return message;
}
}
private class Cause {
private String exception;
private String message;
public Cause(Class<?> exception, String message) {
this.exception = exception.getSimpleName();
this.message = message;
}
public String getException() {
return exception;
}
public String getMessage() {
return message;
}
}
}
Here's my AuthenticationEntryPoint. I've tried placing it in WebSecurityConfigurerAdapter configuration below but it did not work:
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
ObjectMapper objectMapper = new ObjectMapper();
HttpStatus httpStatus = HttpStatus.UNAUTHORIZED;
ApiError apiError = new ApiError(httpStatus, e, "Authentication error");
String jsonResponse = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(apiError);
httpServletResponse.setStatus(httpStatus.value());
httpServletResponse.getOutputStream().print(jsonResponse);
}
Here's the type of default Spring Security responses that I would like to change:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
{
"error": "invalid_token",
"error_description": "Invalid access token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleAiOjE1ODY4NjkyOTYsInVzZXJfbmFtZSI6ImFkaXBvcEBnbWFpbC5jb20iLCJhdXRob3JpdGllcyI6WyJST0xFX01BTkFHRVIiXSwianRpIjoiNGYxZmQ1NTItYmQxNC00NDcyLWJhOTctNjIxY2FlZmYzNGQxIiwiY2xpZW50X2lkIjoic2FmZWRyaXZlLXdlYmFwcCIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdfQ.IMCogr_M8Skw0R0g0pqshROh8-nms8U3zt5i1G7CXO48OcJ76V1WXTGizn5anzFFIRHk0xGhw-r46lgHhLoWB89pjCC04PAIjFwl31flKWVSW6js9QfMt4O8CL6TAXnyHShUyJxbLZnnavTL3b40iLOHNJSeIf7Ed6goqOZMwZUBDB2KKCY_rmu80Ntj69uzVBrVfXCdDW7SRy-05uTqIGlBTWf3v4NZ4lV7EYzTJOcjavkBcSJeLcNi0DpYu1enF4rLPP9MeIUdiWT9sD6FOlLjs_vXQ_rZnxj-TVVkGSYRNn4u3-Znx4WZ3QBbcDU_O_w8qrp5_JnQbXy27-fmpg"
}
{
"error": "invalid_grant",
"error_description": "Bad credentials"
}
Here's how my WebSecurityConfigurerAdapter (the generic security configuration - order 1, the AuthenticationManager is configured in another WebSecurityConfigurerAdapter with order 100) looks like:
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.headers().frameOptions().disable()
.and()
.antMatcher("/console/**")
.authorizeRequests()
.antMatchers("/console/**").hasRole("ADMIN")
.and()
.httpBasic();
}
The ResourceServerConfigurerAdapter:
#Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/actuator/**", "/api-docs/**").permitAll()
.antMatchers("/protected/**").hasAnyRole("ADMIN", "MANAGER")
.anyRequest().authenticated();
}
AuthorizationServerConfigurerAdapter:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
//enables chaining multiple types of claims containing different information
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenConverter));
endpoints.tokenStore(tokenStore)
.accessTokenConverter(tokenConverter)
.authenticationManager(authenticationManager)
.tokenEnhancer(tokenEnhancerChain);
}
How should I set this up? I've looked over countless resources. Which HttpSecurity takes priority? I am a bit confused.
I am doing basic authentication in spring boot . Also i have a filter which does the header filtering and throws error if i dint pass required values in header . Authentication and header filtering are working fine if implemented separately. But if we implement both , i am getting the same response for both the validations ( filter and basic auth ). My guess is as filter response is generated first , it is getting replaced by the authentication response later.
PS: Used ** in code below to tell the issue location .
Any experts please advice . Thanks
#Slf4j
#Component
#Order(Ordered.HIGHEST_PRECEDENCE+2000)
#WebFilter
public class ValidTenantFilter extends OncePerRequestFilter {
#Autowired
private ClientRepository clientRepository;
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
AntPathMatcher pathMatcher = new AntPathMatcher();
return Constant.TENANT_FILTER_URL_LIST.stream()
.anyMatch(p -> pathMatcher.match(p, request.getServletPath()));
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("Inside Tenant Checker filter for path {} with method {} ",request.getServletPath(),request.getMethod());
if(!this.isValidTenant(request)) {
** response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid "+ Constant.X_COMPANY_ID+" and/or "+Constant.X_OPERATOR_ID+ " are passed. Please validate the request."); **
}
else {
filterChain.doFilter(request, response);
}
}
private boolean isValidTenant(HttpServletRequest request) {
// Getting company id and operator id from the header , earlier we were using the tenant id
String companyId = request.getHeader(Constant.X_COMPANY_ID);
String operatorId=request.getHeader(Constant.X_OPERATOR_ID);
if(StringUtils.isNotEmpty(companyId) && StringUtils.isAlphanumeric(companyId)
&& StringUtils.isNotEmpty(operatorId) && StringUtils.isAlphanumeric(operatorId)) {
Client client = clientRepository.findByIdAndOperatorId(companyId, operatorId);
//Only active clients request are entertained. // PRODUCT FIX
if(client!=null && client.getId()!=null && client.isActive()) {
MDC.put(MDC_CLIENT_ID, client.getId().toString());
TenantContext.setCurrentTenant(client.getId().toString());
return true;
}
}
return false;
}
}
And below is the code for the authentication part :
#Configuration
#EnableWebSecurity
#Slf4j
public class SomeConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationEntryPointImpl authEntryPoint;
#Autowired
private ApplicationClients application;
#Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigInDev() {
return new PropertySourcesPlaceholderConfigurer();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable()
.cors().disable();
//http.requiresChannel().antMatchers("/*").requiresSecure();
http.authorizeRequests()
// .antMatchers("/**").hasRole("ADMIN")
//.antMatchers("/user").hasAnyRole("ADMIN")
.anyRequest().authenticated()
.and().httpBasic();
http.headers().defaultsDisabled().cacheControl().and().contentTypeOptions()
.and().frameOptions().deny().xssProtection().block(false)
.and().httpStrictTransportSecurity().includeSubDomains(true).maxAgeInSeconds(31536000);
// Use AuthenticationEntryPoint to authenticate user/password
http.httpBasic().authenticationEntryPoint(authEntryPoint);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources",
"/swagger-resources/configuration/**", "/swagger-ui.html", "/webjars/**");
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(inMemoryUserDetailsManager());
}
#Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
final InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
log.info("Importing {} clients:", application.getClients().size());
application.getClients().forEach(client -> {
String encrytedPassword = this.passwordEncoder().encode(client.getPassword());
manager.createUser(User.withUsername(client.getUsername()).password(encrytedPassword).roles(client.getRoles()).build());
log.info("Imported client {}", client.toString());
});
return manager;
}
}
Code for authentication entry point :
#Component
public class AuthenticationEntryPointImpl extends BasicAuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
throws IOException, ServletException {
//This is invoked when a user tries to access a secured REST resource without supplying any credentials
//We should just add a 401 Unauthorized response because there is no 'login page' to redirect to
** response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println("{\"status\": " + HttpServletResponse.SC_UNAUTHORIZED + ", \"message\": \"" + authEx.getMessage() + "\" }");**
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("api-services");
super.afterPropertiesSet();
}
}
Getting below Response in POSTMAN :
1. If i dont pass any credentials in postman i am getting below response
{
"status": 401,
"message": "Full authentication is required to access this resource"
}
If i dont pass the headers , ideally i should get below response :
{
"timestamp": 1557753285553,
"status": 403,
"error": "Forbidden",
"message": "Invalid X-COMPANY-ID and/or X-OPERATOR-ID are passed. Please validate the request.",
"path": "/apa/invoices"
}
But instead of this i am getting below error :
{
"status": 401,
"message": "Full authentication is required to access this resource"
}
How can I get my custom ResponseEntityExceptionHandler or OAuth2ExceptionRenderer to handle Exceptions raised by Spring security on a pure resource server?
We implemented a
#ControllerAdvice
#RestController
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
so whenever there is an error on the resource server we want it to answer with
{
"message": "...",
"type": "...",
"status": 400
}
The resource server uses the application.properties setting:
security.oauth2.resource.userInfoUri: http://localhost:9999/auth/user
to authenticate and authorize a request against our auth server.
However any spring security error will always bypass our exception handler at
#ExceptionHandler(InvalidTokenException.class)
public ResponseEntity<Map<String, Object>> handleInvalidTokenException(InvalidTokenException e) {
return createErrorResponseAndLog(e, 401);
}
and produce either
{
"timestamp": "2016-12-14T10:40:34.122Z",
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/api/templates/585004226f793042a094d3a9/schema"
}
or
{
"error": "invalid_token",
"error_description": "5d7e4ab5-4a88-4571-b4a4-042bce0a076b"
}
So how do I configure the security exception handling for a resource server? All I ever find are examples on how to customize the Auth Server by implementing a custom OAuth2ExceptionRenderer. But I can't find where to wire this to the resource server's security chain.
Our only configuration/setup is this:
#SpringBootApplication
#Configuration
#ComponentScan(basePackages = {"our.packages"})
#EnableAutoConfiguration
#EnableResourceServer
As noted in previous comments the request is rejected by the security framework before it reaches the MVC layer so #ControllerAdvice is not an option here.
There are 3 interfaces in the Spring Security framework that may be of interest here:
org.springframework.security.web.authentication.AuthenticationSuccessHandler
org.springframework.security.web.authentication.AuthenticationFailureHandler
org.springframework.security.web.access.AccessDeniedHandler
You can create implementations of each of these Interfaces in order to customize the response sent for various events: successful login, failed login, attempt to access protected resource with insufficient permissions.
The following would return a JSON response on unsuccessful login attempt:
#Component
public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler
{
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException ex) throws IOException, ServletException
{
response.setStatus(HttpStatus.FORBIDDEN.value());
Map<String, Object> data = new HashMap<>();
data.put("timestamp", new Date());
data.put("status",HttpStatus.FORBIDDEN.value());
data.put("message", "Access Denied");
data.put("path", request.getRequestURL().toString());
OutputStream out = response.getOutputStream();
com.fasterxml.jackson.databind.ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(out, data);
out.flush();
}
}
You also need to register your implementation(s) with the Security framework. In Java config this looks like the below:
#Configuration
#EnableWebSecurity
#ComponentScan("...")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Override
public void configure(HttpSecurity http) throws Exception
{
http
.addFilterBefore(corsFilter(), ChannelProcessingFilter.class)
.logout()
.deleteCookies("JESSIONID")
.logoutUrl("/api/logout")
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/api/login")
.failureHandler(authenticationFailureHandler())
.successHandler(authenticationSuccessHandler())
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler());
}
/**
* #return Custom {#link AuthenticationFailureHandler} to send suitable response to REST clients in the event of a
* failed authentication attempt.
*/
#Bean
public AuthenticationFailureHandler authenticationFailureHandler()
{
return new RestAuthenticationFailureHandler();
}
/**
* #return Custom {#link AuthenticationSuccessHandler} to send suitable response to REST clients in the event of a
* successful authentication attempt.
*/
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler()
{
return new RestAuthenticationSuccessHandler();
}
/**
* #return Custom {#link AccessDeniedHandler} to send suitable response to REST clients in the event of an attempt to
* access resources to which the user has insufficient privileges.
*/
#Bean
public AccessDeniedHandler accessDeniedHandler()
{
return new RestAccessDeniedHandler();
}
}
In case if you're using #EnableResourceServer, you may also find convenient to extend ResourceServerConfigurerAdapter instead of WebSecurityConfigurerAdapter in your #Configuration class. By doing this, you may simply register a custom AuthenticationEntryPoint by overriding configure(ResourceServerSecurityConfigurer resources) and using resources.authenticationEntryPoint(customAuthEntryPoint()) inside the method.
Something like this:
#Configuration
#EnableResourceServer
public class CommonSecurityConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(customAuthEntryPoint());
}
#Bean
public AuthenticationEntryPoint customAuthEntryPoint(){
return new AuthFailureHandler();
}
}
There's also a nice OAuth2AuthenticationEntryPoint that can be extended (since it's not final) and partially re-used while implementing a custom AuthenticationEntryPoint. In particular, it adds "WWW-Authenticate" headers with error-related details.
You are not able to make use of Spring MVC Exception handler annotations such as #ControllerAdvice because spring security filters kicks in much before Spring MVC.
If you're using token validation URL with config similar to Configuring resource server with RemoteTokenServices in Spring Security Oauth2 which returns HTTP status 401 in case of unauthorized:
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
return tokenService;
}
Implementing custom authenticationEntryPoint as described in other answers (https://stackoverflow.com/a/44372313/5962766) won't work because RemoteTokenService use 400 status and throws unhandled exceptions for other statuses like 401:
public RemoteTokenServices() {
restTemplate = new RestTemplate();
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400) {
super.handleError(response);
}
}
});
}
So you need to set custom RestTemplate in RemoteTokenServices config which would handle 401 without throwing exception:
#Primary
#Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("https://token-validation-url.com");
tokenService.setTokenName("token");
RestOperations restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
#Override
// Ignore 400 and 401
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
}
tokenService.setRestTemplate(restTemplate);
return tokenService;
}
And add dependency for HttpComponentsClientHttpRequestFactory:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
OAuth2ExceptionRenderer is for an Authorization Server. The correct answer is likely to handle it like detailed in this post (that is, ignore that it's oauth and treat it like any other spring security authentication mechanism): https://stackoverflow.com/a/26502321/5639571
Of course, this will catch oauth related exceptions (which are thrown before you reach your resource endpoint), but any exceptions happening within your resource endpoint will still require an #ExceptionHandler method.
We can use this security handler to pass the handler to spring mvc #ControllerAdvice
#Component
public class AuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
private static final Logger LOG = LoggerFactory.getLogger(AuthExceptionHandler.class);
private final HandlerExceptionResolver resolver;
#Autowired
public AuthExceptionHandler(#Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
LOG.error("Responding with unauthorized error. Message - {}", authException.getMessage());
resolver.resolveException(request, response, null, authException);
}
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
LOG.error("Responding with access denied error. Message - {}", accessDeniedException.getMessage());
resolver.resolveException(request, response, null, accessDeniedException);
}
}
Then define the exception by using #ControllerAdvice so that we can manage the global exception handler in one place..
This is possible. Since the original question is for a REST controller that needs to return a custom JSON response, I will write up a complete answer step by step which worked for me. First and foremost, it seems you cannot handle this with a #ControllerAdvice that extends ControllResponseEntityExceptionHandler. You need a separate handler that extends AccessDeniedHandler. Follow the below steps.
Step 1: Create a custom handler class that extends AccessDeniedHandler
#Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private static final String JSON_TYPE = "application/json";
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
MyErrorList errors = new MyErrorList();
errors.addError(new MyError("", "You do not have permission to access this resource."));
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(JSON_TYPE);
OutputStream output = response.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(output, errors);
output.flush();
}
}
'MyError' above is a simple POJO to represent an error json structure and MyErrorList is another POJO that holds a list of 'MyError's.
Step 2: Inject the Handler created above into the Security configuration
#Autowired
private VOMSAccessDeniedHandler accessDeniedHandler;
Step 3: Register the accessDeniedHandler in your configure method
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
With Step 2 and Step 3, Your SecurityConfiguration should look something like this (Note that I am omitting code that is not relevant to this problem to shorten the length of this answer):
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private MyAccessDeniedHandler accessDeniedHandler;
// Other stuff
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/register").permitAll()
.antMatchers("/authenticate").permitAll()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Adapting the accepted answer for use with Spring OAuth2ResourceServer for JWT authentication, because without special configuration, it will register its own BearerTokenAuthenticationEntryPoint, and ignore the one we set in .exceptionHandling().authenticationEntryPoint()
Hence, in our WebSecurityConfigurerAdapter we have:
#Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
protected void configure(HttpSecurity http) throws Exception {
http
// ... all the usual stuff ...
// configure OAuth2 (OIDC) JWT and set a custom authentication failure handler
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt().and()
.authenticationEntryPoint(authenticationFailureHandler));
}
where AuthenticationFailureHandler is coded as suggested in earlier answers:
#Component
public class AuthenticationFailureHandler implements AuthenticationEntryPoint {
public AuthenticationFailureHandler() {
}
// Autowire our own CustomExceptionHandler: must be qualified because Spring Boot has others in the classpath
#Autowired
#Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws
IOException {
resolver.resolveException(request, response, null, authException);
}
}
In our CustomExceptionHandler (which is autowired above, but not mentioned by class name explicitly) we add a method for AuthenticationException handling:
#ExceptionHandler(value = {AuthenticationException.class})
protected ResponseEntity<?> handleAuthenticationException(RuntimeException ex, WebRequest request) {
return ... something ... // create custom error response here
}
Spring 3.0 Onwards,You can use #ControllerAdvice (At Class Level) and extends org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler class from CustomGlobalExceptionHandler
#ExceptionHandler({com.test.CustomException1.class,com.test.CustomException2.class})
public final ResponseEntity<CustomErrorMessage> customExceptionHandler(RuntimeException ex){
return new ResponseEntity<CustomErrorMessage>(new CustomErrorMessage(false,ex.getMessage(),404),HttpStatus.BAD_REQUEST);
}