Jersey ContainerResponseFilter not applied to all responses - filter

I have a Dropwizard 1.0.0 application, using a ContainerResponseFilter I expect to see a series of headers applied to each resource - however I only see them applied to the root level document.
Filter class:
package com.uk.jacob.filters;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.MultivaluedMap;
import java.io.IOException;
public class SecurityFilter implements ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException {
MultivaluedMap<String, Object> headers = containerResponseContext.getHeaders();
headers.add("strict-transport-security", "max-age=31536000");
headers.add("x-content-type-options", "nosniff");
headers.add("x-frame-options", "SAMEORIGIN");
headers.add("x-xss-protection", "1; mode=block");
}
}
Application class:
package com.uk.jacob;
import com.uk.jacob.filters.SecurityFilter;
import com.uk.jacob.resources.HomepageResource;
import io.dropwizard.Application;
import io.dropwizard.assets.AssetsBundle;
import io.dropwizard.client.HttpClientBuilder;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import io.dropwizard.views.ViewBundle;
import org.apache.http.client.HttpClient;
public class websiteApplication extends Application<websiteConfiguration> {
public static void main(final String[] args) throws Exception {
new websiteApplication().run(args);
}
#Override
public String getName() {
return "website";
}
#Override
public void initialize(final Bootstrap<websiteConfiguration> bootstrap) {
bootstrap.addBundle(new ViewBundle<websiteConfiguration>());
bootstrap.addBundle(new AssetsBundle("/public/", "/public"));
}
#Override
public void run(final websiteConfiguration configuration, final Environment environment) {
final HttpClient httpClient = new HttpClientBuilder(environment).using(configuration.getHttpClientConfiguration()).build(getName());
environment.jersey().register(new SecurityFilter());
environment.jersey().register(new HomepageResource(httpClient));
}
}

your issue is the AssetBundle. AssetBundles are separate servlets, they do not go through the jersey eco-system. From the source:
#Override
public void run(Environment environment) {
LOGGER.info("Registering AssetBundle with name: {} for path {}", assetsName, uriPath + '*');
environment.servlets().addServlet(assetsName, createServlet()).addMapping(uriPath + '*');
}
This is why your filter is not called on an asset resource. The solution to this would be to write an extra ServletFilter (old style) that filters asset requests and adds your headers there.
In Order to also add your headers to your asset bundles you must use a normal servlet filter and register it with DW:
Here is the code for my ServletFilter:
public class ServletRequestFilter implements Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("Asset filter");
HttpServletResponse httpServletResponse = ((HttpServletResponse) response);
httpServletResponse.addHeader("ARTUR", "test");
chain.doFilter(request, httpServletResponse);
}
#Override
public void destroy() {
}
}
This is then register as follows:
public class ViewApplication extends io.dropwizard.Application<Configuration>{
#Override
public void run(Configuration configuration, Environment environment) throws Exception {
environment.jersey().register(ViewResource.class);
environment.jersey().register(HeaderResponseFilter.class);
environment.servlets().addFilter("Custom-Filter-Name", new ServletRequestFilter()).addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/assets/*");
}
#Override
public void initialize(Bootstrap<Configuration> bootstrap) {
super.initialize(bootstrap);
bootstrap.addBundle(new ViewBundle<>());
bootstrap.addBundle(new AssetsBundle("/assets/", "/assets"));
}
public static void main(String[] args) throws Exception {
new ViewApplication().run("server", "/home/artur/dev/repo/sandbox/src/main/resources/config/test.yaml");
}
}
Note: I am registering an asset bundle for the urls in /assets/. This is also noted in the registration of my filter (which filters only assets resources). Alternatively, you can match it on all calls and filter everything through that (that way you can get rid of the jersey counterpart I believe).
Finally the test for my resource:
artur#pandaadb:~/dev/repo/sandbox$ curl -v "http://localhost:9085/assets/test.txt"
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9085 (#0)
> GET /assets/test.txt HTTP/1.1
> Host: localhost:9085
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Mon, 19 Sep 2016 15:07:13 GMT
< ARTUR: test
< Last-Modified: Mon, 19 Sep 2016 14:52:37 GMT
< ETag: "0d1ae97d61a8900c99abddb8741febaf"
< Content-Type: text/plain;charset=utf-8
< Vary: Accept-Encoding
< Content-Length: 11
<
asd
asd
* Connection #0 to host localhost left intact
Note my custom Header ARTUR: test
Regards,
artur

Related

How to prevent Redis writes for anonymous user sessions

I have this sample application:
package com.example.session;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication
public class DemoRedisDataSessionApplication {
#Configuration
#EnableWebSecurity
#EnableRedisHttpSession(redisNamespace = "demo-redis-data-session")
public static class AppConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("0000").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and()
.authorizeRequests().antMatchers("/ping").permitAll().and()
.authorizeRequests().anyRequest().fullyAuthenticated();
}
}
#RestController
public static class AppController {
#GetMapping("/ping")
public String ping() {
return "pong";
}
#GetMapping("/secured")
public String secured() {
return "secured";
}
}
public static void main(String[] args) {
SpringApplication.run(DemoRedisDataSessionApplication.class, args);
}
}
When I hit /secured I get 302 redirected to the /login form, which is what I expect if I am not logged in, but I get some unwanted entries in Redis:
127.0.0.1:6379> keys *
1) "spring:session:demo-redis-data-session:sessions:expires:dbb124b9-c37d-454c-8d67-409f28cb88a6"
2) "spring:session:demo-redis-data-session:expirations:1515426060000"
3) "spring:session:demo-redis-data-session:sessions:dbb124b9-c37d-454c-8d67-409f28cb88a6"
I don't want to create this data for every anonymous user (read crawler), so is there a way to prevent these Redis entries when hitting a secured endpoint/page with an anonymous user?
Additional data used for this sample project
 docker-compose.yml
version: "2"
services:
redis:
image: redis
ports:
- "6379:6379"
Spring Boot version
1.5.9.RELEASE
This is not the optimal solution since it creates only one session for all crawlers, but at least I don't get Redis full of unwanted session.
import lombok.extern.log4j.Log4j;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.CookieHttpSessionStrategy;
import org.springframework.session.web.http.MultiHttpSessionStrategy;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
#Log4j
#Component
public class CrawlerManagerSessionStrategyWrapper implements MultiHttpSessionStrategy {
private CookieHttpSessionStrategy delegate;
private volatile String crawlerSessionId;
public CrawlerManagerSessionStrategyWrapper() {
this.delegate = new CookieHttpSessionStrategy();
}
public String getRequestedSessionId(HttpServletRequest request) {
String sessionId = getSessionIdForCrawler(request);
if (sessionId != null)
return sessionId;
else {
return delegate.getRequestedSessionId(request);
}
}
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
delegate.onNewSession(session, request, response);
if (isCrawler(request)) {
crawlerSessionId = session.getId();
}
}
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
delegate.onInvalidateSession(request, response);
}
public HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response) {
return request;
}
public HttpServletResponse wrapResponse(HttpServletRequest request, HttpServletResponse response) {
return response;
}
private String getSessionIdForCrawler(HttpServletRequest request) {
if (isCrawler(request)) {
SessionRepository<Session> repo = (SessionRepository<Session>) request.getAttribute(SessionRepository.class.getName());
if (crawlerSessionId != null && repo != null) {
Session session = repo.getSession(crawlerSessionId);
if (session != null) {
return crawlerSessionId;
}
}
}
return null;
}
private boolean isCrawler(HttpServletRequest request) {
// Here goes the logic to understand if the request comes from a crawler, for example by checking the user agent.
return true;
}
}
The only thing to implement is the isCrawler method to state if the request comes from a crawler.

How to use ContentCachingResponseWrapper to read httpServletResponse? [duplicate]

I'm trying to implement filter for logging requests and responses in Spring MVC application.
I use the following code:
#Component
public class LoggingFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
LOGGER.debug(REQUEST_MESSAGE_FORMAT, requestWrapper.getRequestURI(), requestWrapper.getMethod(), requestWrapper.getContentType(),
new ServletServerHttpRequest(requestWrapper).getHeaders(), IOUtils.toString(requestWrapper.getInputStream(), UTF_8));
filterChain.doFilter(requestWrapper, responseWrapper);
LOGGER.debug(RESPONSE_MESSAGE_FORMAT, responseWrapper.getStatus(), responseWrapper.getContentType(),
new ServletServerHttpResponse(responseWrapper).getHeaders(), IOUtils.toString(responseWrapper.getContentInputStream(), UTF_8));
}
}
So, I get my request and respone logged as expected. Here are the logs:
2016-10-08 19:10:11.212 DEBUG 11072 --- [qtp108982313-19] by.kolodyuk.logging.LoggingFilter
----------------------------
ID: 1
URI: /resources/1
Http-Method: GET
Content-Type: null
Headers: {User-Agent=[curl/7.41.0], Accept=[*/*], Host=[localhost:9015]}
Body:
--------------------------------------
2016-10-08 19:10:11.277 DEBUG 11072 --- [qtp108982313-19] by.kolodyuk.logging.LoggingFilter
----------------------------
ID: 1
Response-Code: 200
Content-Type: application/json;charset=UTF-8
Headers: {}
Body: {"id":"1"}
--------------------------------------
However, the empty response is returned. Here's the output from curl:
$ curl http://localhost:9015/resources/1 --verbose
* Trying ::1...
* Connected to localhost (::1) port 9015 (#0)
> GET /resources/1 HTTP/1.1
> User-Agent: curl/7.41.0
> Host: localhost:9015
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 08 Oct 2016 17:10:11 GMT
< Content-Type: application/json;charset=UTF-8
< Content-Length: 0
<
* Connection #0 to host localhost left intact
Any ideas?
Thanks
After couple of hours of struggling, I've finally found the solution.
In short, ContentCachingResponseWrapper.copyBodyToResponse() should be called in the end of the filter method.
ContentCachingResponseWrapper caches the response body by reading it from response output stream. So, the stream becomes empty. To write response back to the output stream ContentCachingResponseWrapper.copyBodyToResponse() should be used.
Finally solved the problem. Here is the perfect solution:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Enumeration;
import java.util.Map;
import static java.nio.charset.StandardCharsets.UTF_8;
import static net.logstash.logback.marker.Markers.appendFields;
#Component
public class LoggingFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);
#Autowired
private ObjectMapper objectMapper;
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpServletRequest);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpServletResponse);
filterChain.doFilter(requestWrapper, responseWrapper);
String requestUrl = requestWrapper.getRequestURL().toString();
HttpHeaders requestHeaders = new HttpHeaders();
Enumeration headerNames = requestWrapper.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
requestHeaders.add(headerName, requestWrapper.getHeader(headerName));
}
HttpMethod httpMethod = HttpMethod.valueOf(requestWrapper.getMethod());
Map<String, String[]> requestParams = requestWrapper.getParameterMap();
String requestBody = IOUtils.toString(requestWrapper.getInputStream(),UTF_8);
JsonNode requestJson = objectMapper.readTree(requestBody);
RequestEntity<JsonNode> requestEntity = new RequestEntity<>(requestJson,requestHeaders, httpMethod, URI.create(requestUrl));
LOGGER.info(appendFields(requestEntity),"Logging Http Request");
HttpStatus responseStatus = HttpStatus.valueOf(responseWrapper.getStatusCode());
HttpHeaders responseHeaders = new HttpHeaders();
for (String headerName : responseWrapper.getHeaderNames()) {
responseHeaders.add(headerName, responseWrapper.getHeader(headerName));
}
String responseBody = IOUtils.toString(responseWrapper.getContentInputStream(), UTF_8);
JsonNode responseJson = objectMapper.readTree(responseBody);
ResponseEntity<JsonNode> responseEntity = new ResponseEntity<>(responseJson,responseHeaders,responseStatus);
LOGGER.info(appendFields(responseEntity),"Logging Http Response");
responseWrapper.copyBodyToResponse();
}
}
The pattern I like to use is to split this into 2 filters, one for extracting the raw body and another one to do the logging - feels a more SRP.
#Slf4j // lombok logging
#Component // spring loads filter into it's filter chain
#Order(1) // Best if this goes first (or early in filter chain)
public class CachingBodyFilter implements Filter {
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
ContentCachingRequestWrapper reqWrapper = new ContentCachingRequestWrapper((HttpServletRequest) req);
ContentCachingResponseWrapper resWrapper = new ContentCachingResponseWrapper((HttpServletResponse) res);
try {
chain.doFilter(reqWrapper, resWrapper);
resWrapper.copyBodyToResponse(); // Necessary (see answer by StasKolodyuk above)
} catch (IOException | ServletException e) {
log.error("Error extracting body", e);
}
}
}
And then we create another filter to do the logging part.
#Slf4j
#Component
#Order(2) // This needs to come after `CachingBodyFilter`
public class PayloadLogFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
chain.doFilter(req, res);
if (req instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper reqWrapper = (ContentCachingRequestWrapper) req;
String payload = new String (reqWrapper.getContentAsByteArray(), "utf-8");
log.debug("Request [ {} ] has payload [ {} ]", reqWrapper.getRequestURI(), payload);
}
}
}
A nice advantage of splitting these up is that other classes (e.g. a Spring AOP interceptor or a Spring controller) can also access / use the HTTP body.

Spring Security Oauth - Custom format for OAuth2Exceptions

The error format of spring security oauth conforms with the OAuth spec and looks like this.
{
"error":"insufficient_scope",
"error_description":"Insufficient scope for this resource",
"scope":"do.something"
}
Especially on a resource server I find it a bit strange to get a different error format for authentication issues. So I would like to change the way this exception is rendered.
The documentation says
Error handling in an Authorization Server uses standard Spring MVC
features, namely #ExceptionHandler methods
So I tried something like this to customize the format of the error:
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class MyErrorHandler {
#ExceptionHandler(value = {InsufficientScopeException.class})
ResponseEntity<MyErrorRepresentation> handle(RuntimeException ex, HttpServletRequest request) {
return errorResponse(HttpStatus.FORBIDDEN,
MyErrorRepresentation.builder()
.errorId("insufficient.scope")
.build(),
request);
}
}
But this does not work.
Looking at the code, all the error rendering seems to be done in DefaultWebResponseExceptionTranslator#handleOAuth2Exception. But implementing a custom WebResponseExceptionTranslator would not allow changing the format.
Any hints?
First of all,some knowledge for Spring Security OAuth2.
OAuth2 has two main parts
AuthorizationServer : /oauth/token, get token
ResourceServer : url resource priviledge management
Spring Security add filter to the filter chains of server container, so the exception of Spring Security will not reach #ControllerAdvice
Then, custom OAuth2Exceptions should consider for AuthorizationServer and ResourceServer.
This is configuration
#Configuration
#EnableAuthorizationServer
public class OAuthSecurityConfig extends AuthorizationServerConfigurerAdapter {
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//for custom
endpoints.exceptionTranslator(new MyWebResponseExceptionTranslator());
}
}
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
// format message
resources.authenticationEntryPoint(new MyAuthenticationEntryPoint());
resources.accessDeniedHandler(new MyAccessDeniedHandler());
}
}
MyWebResponseExceptionTranslator is translate the exception to ourOAuthException and we custom ourOAuthException serializer by jackson, which way is same by default the OAuth2 use.
#JsonSerialize(using = OAuth2ExceptionJackson1Serializer.class)
public class OAuth2Exception extends RuntimeException {
other custom handle class stuff
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
/**
* #author qianggetaba
* #date 2019/6/21
*/
public class MyWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
#Override
public ResponseEntity<OAuth2Exception> translate(Exception exception) throws Exception {
if (exception instanceof OAuth2Exception) {
OAuth2Exception oAuth2Exception = (OAuth2Exception) exception;
return ResponseEntity
.status(oAuth2Exception.getHttpErrorCode())
.body(new CustomOauthException(oAuth2Exception.getMessage()));
}else if(exception instanceof AuthenticationException){
AuthenticationException authenticationException = (AuthenticationException) exception;
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(new CustomOauthException(authenticationException.getMessage()));
}
return ResponseEntity
.status(HttpStatus.OK)
.body(new CustomOauthException(exception.getMessage()));
}
}
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
/**
* #author qianggetaba
* #date 2019/6/21
*/
#JsonSerialize(using = CustomOauthExceptionSerializer.class)
public class CustomOauthException extends OAuth2Exception {
public CustomOauthException(String msg) {
super(msg);
}
}
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
/**
* #author qianggetaba
* #date 2019/6/21
*/
public class CustomOauthExceptionSerializer extends StdSerializer<CustomOauthException> {
public CustomOauthExceptionSerializer() {
super(CustomOauthException.class);
}
#Override
public void serialize(CustomOauthException value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeNumberField("code4444", value.getHttpErrorCode());
jsonGenerator.writeBooleanField("status", false);
jsonGenerator.writeObjectField("data", null);
jsonGenerator.writeObjectField("errors", Arrays.asList(value.getOAuth2ErrorCode(),value.getMessage()));
if (value.getAdditionalInformation()!=null) {
for (Map.Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
String key = entry.getKey();
String add = entry.getValue();
jsonGenerator.writeStringField(key, add);
}
}
jsonGenerator.writeEndObject();
}
}
for custom ResourceServer exception
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* #author qianggetaba
* #date 2019/6/21
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException)
throws ServletException {
Map map = new HashMap();
map.put("errorentry", "401");
map.put("message", authException.getMessage());
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(new Date().getTime()));
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), map);
} catch (Exception e) {
throw new ServletException();
}
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* #author qianggetaba
* #date 2019/6/21
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler{
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Map map = new HashMap();
map.put("errorauth", "400");
map.put("message", accessDeniedException.getMessage());
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(new Date().getTime()));
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), map);
} catch (Exception e) {
throw new ServletException();
}
}
}
I found a similar question with answers that really helped my solving this - Handle spring security authentication exceptions with #ExceptionHandler
But my question is specifically about spring-security-oauth2 - so I think it is still worth stating the answer specific to spring-security-oauth2. My solution was picked from different answers to the question mentioned above.
My samples work for spring-security-oauth2 2.0.13
So the solution for me to achieve a different custom error structure for oauth2 errors on resource server resources was to register a custom OAuth2AuthenticationEntryPoint and OAuth2AccessDeniedHandler that I register using a ResourceServerConfigurerAdapter. It is worth mentioning that this is only changing the format for ResourceServer endpoints - and not the AuthorizationServer endpoints like the TokenEndpoint.
class MyCustomOauthErrorConversionConfigurerAdapter extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer configurer) throws Exception {
configurer.authenticationEntryPoint(new MyCustomOauthErrorOAuth2AuthenticationEntryPoint());
configurer.accessDeniedHandler(new MyCustomOauthErrorOAuth2AccessDeniedHandler());
}
}
I could not reuse the functionality in OAuth2AuthenticationEntryPoint and OAuth2AccessDeniedHandler because the relevant methods translate the exception and flush it in the same method. So I needed to copy some code:
public class MyCustomOauthErrorOAuth2AccessDeniedHandler extends OAuth2AccessDeniedHandler {
private final MyCustomOauthErrorOAuth2SecurityExceptionHandler oAuth2SecurityExceptionHandler = new MyCustomOauthErrorOAuth2SecurityExceptionHandler();
/**
* Does exactly what OAuth2AccessDeniedHandler does only that the body is transformed to {#link MyCustomOauthError} before rendering the exception
*/
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, org.springframework.security.access.AccessDeniedException authException)
throws IOException, ServletException {
oAuth2SecurityExceptionHandler.handle(request, response, authException, this::enhanceResponse);
}
}
public class ExceptionMessageOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
private final MyCustomOauthErrorOAuth2SecurityExceptionHandler oAuth2SecurityExceptionHandler = new MyCustomOauthErrorOAuth2SecurityExceptionHandler();
/**
* Does exactly what OAuth2AuthenticationEntryPoint does only that the body is transformed to {#link MyCustomOauthError} before rendering the exception
*/
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
oAuth2SecurityExceptionHandler.handle(request, response, authException, this::enhanceResponse);
}
}
#RequiredArgsConstructor
public class MyCustomOauthErrorOAuth2SecurityExceptionHandler {
private final WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();
private final OAuth2ExceptionRenderer exceptionRenderer = new DefaultOAuth2ExceptionRenderer();
private final HandlerExceptionResolver handlerExceptionResolver = new DefaultHandlerExceptionResolver();
/**
* This is basically what {#link org.springframework.security.oauth2.provider.error.AbstractOAuth2SecurityExceptionHandler#doHandle(HttpServletRequest, HttpServletResponse, Exception)} does.
*/
public void handle(HttpServletRequest request, HttpServletResponse response, RuntimeException authException,
BiFunction<ResponseEntity<OAuth2Exception>, Exception, ResponseEntity<OAuth2Exception>> oauthExceptionEnhancer)
throws IOException, ServletException {
try {
ResponseEntity<OAuth2Exception> defaultErrorResponse = exceptionTranslator.translate(authException);
defaultErrorResponse = oauthExceptionEnhancer.apply(defaultErrorResponse, authException);
//this is the actual translation of the error
final MyCustomOauthError customErrorPayload =
MyCustomOauthError.builder()
.errorId(defaultErrorResponse.getBody().getOAuth2ErrorCode())
.message(defaultErrorResponse.getBody().getMessage())
.details(defaultErrorResponse.getBody().getAdditionalInformation() == null ? emptyMap() : defaultErrorResponse.getBody().getAdditionalInformation())
.build();
final ResponseEntity<MyCustomOauthError> responseEntity = new ResponseEntity<>(customErrorPayload, defaultErrorResponse.getHeaders(), defaultErrorResponse.getStatusCode());
exceptionRenderer.handleHttpEntityResponse(responseEntity, new ServletWebRequest(request, response));
response.flushBuffer();
} catch (ServletException e) {
// Re-use some of the default Spring dispatcher behaviour - the exception came from the filter chain and
// not from an MVC handler so it won't be caught by the dispatcher (even if there is one)
if (handlerExceptionResolver.resolveException(request, response, this, e) == null) {
throw e;
}
} catch (IOException | RuntimeException e) {
throw e;
} catch (Exception e) {
// Wrap other Exceptions. These are not expected to happen
throw new RuntimeException(e);
}
}
}
Spring Boot version: 2.2.5
You really don't have to write that much code. All you need to do create a custom AuthenticationEntryPoint by extending OAuth2AuthenticationEntryPoint, override enhanceResponse method of it and register it via Resource Server configuration.
First part:
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer config) {
config.authenticationEntryPoint(new CustomOauth2AuthenticationEntryPoint());
}
}
Second part:
public class CustomOauth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {
#Override
protected ResponseEntity<String> enhanceResponse(ResponseEntity<?> response, Exception exception) {
return ResponseEntity.status(response.getStatusCode()).body("My custom response body.");
}
}
Keep in mind that according to spec 401 response must send WWW-Authenticate header. The enhanceResponse that we override sends that header. Take a look at the implementation and send that header if you return 401.
You have to setAuthenticationEntryPoint in TokenEndpointAuthenticationFilter Bean if you config by AuthorizationServer
#Bean
public TokenEndpointAuthenticationFilter tokenEndpointAuthenticationFilter() {
CustomOauth2AuthenticationEntryPoint entryPoint = new CustomOauth2AuthenticationEntryPoint();
TokenEndpointAuthenticationFilter filter = new TokenEndpointAuthenticationFilter(authenticationManager, requestFactory());
filter.setAuthenticationEntryPoint(entryPoint);
return filter;
}

SpringBoot - Filters exception handler

I have a spring-boot application. I have a class that is ControllerAdvice to handle the exceptions thrown by the app.
I created a filter that I want to use it to validate a header for all my requests. When I throw the custom exception from that filter, it does not go through my exception handler, it uses the default Spring error control.
Is there any way to handle the errors from filters in my own way?
If you catch the exception in a Filter, then handle with HandlerExceptionResolver, the #ControllerAdvice will start to work
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
#Component
public class HystrixRequestContextServletFilter extends OncePerRequestFilter {
private static final String API_KEY = "apiKey";
private final HandlerExceptionResolver handlerExceptionResolver;
#Autowired
public HystrixRequestContextServletFilter(HandlerExceptionResolver handlerExceptionResolver) {
this.handlerExceptionResolver = handlerExceptionResolver;
}
#Override
public void doFilterInternal(HttpServletRequest httpRequest, HttpServletResponse httpResponse, FilterChain filterChain) {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
String apiKey = httpRequest.getHeader(API_KEY);
if (apiKey == null) {
throw new AuthenticationException("no apikey in request, path [" + httpRequest.getRequestURI() + "]");
}
ApiKeyHystrixRequestVariable.set(apiKey);
filterChain.doFilter(httpRequest, httpResponse);
} catch (Exception e) {
handlerExceptionResolver.resolveException(httpRequest, httpResponse, null, e);
} finally {
context.shutdown();
}
}
#Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getRequestURI().startsWith("/internal");
}
}
You can write a controller that extends BasicErrorController and write a method that has a #GetMapping annotation like this:
#RestController
public class FilterExceptionController extends BasicErrorController {
private static final Logger LOGGER = LoggerFactory.getLogger(FilterExceptionController.class);
public FilterExceptionController(){
super(new DefaultErrorAttributes(),new ErrorProperties());
}
#GetMapping
private <T> ResponseResult<T> serviceExceptionHandler(HttpServletRequest request) {
Map<String,Object> body= getErrorAttributes(request,isIncludeStackTrace(request,MediaType.ALL));
String message = String.valueOf(body.get("message"));
body.forEach((k,v)->LOGGER.info("{} ==> {}",k,v));
return RestResultGenerator.genError(500,"Filter Error Occurred, Message: [ "+message+" ]");
}
#Override
public String getErrorPath() {
return "/error";
}
}
There is my test filter:
#WebFilter(filterName = "ipFilter",urlPatterns = "/*")
public class IpFilter implements Filter{
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(!Arrays.asList(new String[]{"127.0.0.1"}).contains(servletRequest.getRemoteAddr())){
throw new ServiceException(403,"ip forbid");
}
}
#Override
public void destroy() {
}
}
This is result(but only get exception message not code):
enter image description here

RESTEASY -Spring Exception mapper for javax.ws.rs.NotFoundException is not getting Involed

Hi I am using Reasteasy 3 along spring 3.
I have defined multiple Exception mappers for my project, and most of them are getting invoked whenever concerned Exceptions are raised.
But I am facing problem with **“javax.ws.rs.NotFoundException” when this exception is raised its handler is not getting invoked.**
All Exception mappers including mapper for “NotFoundException” exception are defined/configured in same manner and they are getting invoked except mapper for NotFoundException.
Is there any different way to configure Exception mappers for JAX-RS exception ..please help I am stuck here .
code for Exception mappers
package com.xyz.exception;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
#Provider
public class CCDBNotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
#Override
public Response toResponse(NotFoundException e) {
// Logs and code
}
}
other Exception mapper configured and defined in a Excatly similar way are getting Invoked ... see snap shot for working Exception mapper
package com.xyz.exception;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.jboss.resteasy.plugins.providers.jaxb.JAXBUnmarshalException;
#Provider
public class CCDBJAXBUnmarshalExceptionMapper implements ExceptionMapper<JAXBUnmarshalException> {
#Override
public Response toResponse(NotFoundException e) {
// Logs and code
}
}
package com.xyz.exception;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
#Provider
public class CCDBApplicationExceptionMapper implements ExceptionMapper<CCDBApplicationException>{
#Override
public Response toResponse(NotFoundException e) {
// Logs and code
}
}
Spring Configuration for provider annotation
<!-- Auto Reegistry of RESTEasy providers -->
<context:component-scan base-package="com.xyz">
<context:include-filter type="annotation" expression="javax.ws.rs.ext.Provider"/>
</context:component-scan>
I tried everything but Exception mappers for any "javax.ws.rs.*" exceptions are not getting invokes although they are getting registered .
to overcome this problem i have used servlet filter ... i posting my code to help others whoever is facing same issues
Register below filter in your web.xml file.
filter code :
public class CCDBExceptionFilter implements Filter {
Logger logger = Logger.getLogger(CCDBExceptionFilter.class);
private static final String NOTFOUND_MESSAGE = "{\"ErrorResponse\":{\"errorCode\":1,\"errorMessage\":\"Invalid request, There is no service configured to handle provided request\"}}";
public void init(FilterConfig filterConfig) throws ServletException {
logger.debug("CCDBExceptionFilter got initiated" );
}
public void doFilter(ServletRequest request, ServletResponse response,FilterChain filterChain)throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
HttpServletRequest req = (HttpServletRequest) request;
logger.debug("Request recived to process " + req.getPathInfo());
DummyResponse dummyResponse = new DummyResponse((HttpServletResponse)response);
filterChain.doFilter(request, dummyResponse);
logger.debug("Request handled for " + req.getPathInfo());
if(404 == dummyResponse.getErrorcode()){
logger.debug("Found 404 error code" );
res.setStatus(400);
PrintWriter out = response.getWriter();
out.println(NOTFOUND_MESSAGE);
out.close();
}
}
public void destroy() {
}
}
Dummy Response class
public class DummyResponse extends HttpServletResponseWrapper {
Logger logger = Logger.getLogger(DummyResponse.class);
private CharArrayWriter buffer; // This can be used as an Writer
int errorcode;
public int getErrorcode() {
return errorcode;
}
public void setErrorcode(int errorcode) {
this.errorcode = errorcode;
}
public DummyResponse(HttpServletResponse response) {
super(response);
buffer = new CharArrayWriter();
}
public String toString() {
return buffer.toString();
}
public PrintWriter getWriter() {
return new PrintWriter(buffer);
}
public void setStatus(int sc) {
if (404 == sc) {
setErrorcode(404);
super.setStatus(400);
} else {
super.setStatus(sc);
}
}
public void sendError(int sc, String msg) throws IOException {
if (404 == sc) {
setErrorcode(404);
super.setStatus(400);
} else {
super.setStatus(sc);
}
}
public void sendError(int sc) throws IOException {
if (404 == sc) {
setErrorcode(404);
super.setStatus(400);
} else {
super.setStatus(sc);
}
}
}

Resources