How to add a header on an auth redirect response with Spring? - spring

For integration of Spring Boot with htmx, I need a way to add a header if an incoming request is done by htmx and the user is no longer logged on.
In the normal flow, the user gets redirected to the login page. However, when there is a request done by htmx, this is an AJAX request and the redirect is not happening.
The recommended solution is that if there is an HX-Request header on the request, that the server should put an HX-Refresh: true header on the response. This will make htmx do a full page refresh.
My security config looks like this:
#Configuration
public class WebSecurityConfiguration {
private final ClientRegistrationRepository clientRegistrationRepository;
public WebSecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeRequests(registry -> {
registry.mvcMatchers("/actuator/info", "/actuator/health").permitAll();
registry.mvcMatchers("/**").hasAuthority(Roles.ADMIN);
registry.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
registry.anyRequest().authenticated();
});
http.oauth2Client();
http.oauth2Login();
http.logout(logout -> logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));
return http.build();
}
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
// Sets the location that the End-User's User Agent will be redirected to
// after the logout has been performed at the Provider
logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
return logoutSuccessHandler;
}
}
I tried with a Filter:
public Filter htmxFilter() {
return new Filter() {
#Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
filterChain.doFilter(servletRequest, servletResponse);
String htmxRequestHeader = request.getHeader("HX-Request");
System.out.println("htmxRequestHeader = " + htmxRequestHeader);
System.out.println(response.getStatus());
if (htmxRequestHeader != null
&& response.getStatus() == 302) {
System.out.println("XXXXXXXXXXX");
response.setHeader("HX-Refresh", "true");
}
}
};
}
But response.getStatus() is never 302 (altough I can see the 302 response status in Chrome).
I also tried with an Interceptor:
#Bean
public HandlerInterceptor htmxHandlerInterceptor() {
return new HandlerInterceptor() {
#Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
boolean htmxRequest = request.getHeader("HX-Request") != null;
String htmxRequestHeader = request.getHeader("HX-Request");
System.out.println("htmxRequestHeader = " + htmxRequestHeader);
System.out.println(response.getStatus());
if( htmxRequest && response.getStatus() == 302) {
response.setHeader("HX-Refresh", "true");
}
}
};
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeInterceptor());
registry.addInterceptor(htmxHandlerInterceptor());//.order(Ordered.HIGHEST_PRECEDENCE);
}
Which also does not work, there is no 302 response status.
I also tried with the commented out order(Ordered.HIGHEST_PRECEDENCE), but that did not make any difference.
Are there other options?

When a request comes to a protected endpoint and it is not authenticated, Spring Security executes its AuthenticationEntryPoints interface to commence an authentication scheme.
You could create your own AuthenticationEntryPoint that adds the header and delegates to the LoginUrlAuthenticationEntryPoint (or other implementation that you are using).
#Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {
http
//...
.exceptionHandling(exception -> exception
.authenticationEntryPoint(new HxRefreshHeaderAuthenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))
);
return http.build();
}
public class HxRefreshHeaderAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final AuthenticationEntryPoint delegate;
public HxRefreshHeaderAuthenticationEntryPoint(AuthenticationEntryPoint delegate) {
this.delegate = delegate;
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// Add the header
this.delegate.commence(request, response, authException);
}
}

You need to make sure that your filter runs before any Spring Security filter. See at SecurityProperties.DEFAULT_FILTER_ORDER or HttpSecurity#addFilterBefore

Related

spring-boot Error: Exceeded maxRedirects. Probably stuck in a redirect loop

I am trying to perform JWT auth in spring boot and the request are getting stuck in redirect loop.
JWTAuthenticationProvider
#Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#Autowired
private JwtUtil jwtUtil;
#Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
#Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
#Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
String token = jwtAuthenticationToken.getToken();
JwtParsedUser parsedUser = jwtUtil.parseToken(token);
if (parsedUser == null) {
throw new JwtException("JWT token is not valid");
}
UserDetails user = User.withUsername(parsedUser.getUserName()).password("temp_password").authorities(parsedUser.getRole()).build();
return user;
}
JwtAuthenticationFilter
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super("/**");
this.setAuthenticationManager(authenticationManager);
}
#Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
throw new JwtException("No JWT token found in request headers");
}
String authToken = header.substring(7);
JwtAuthenticationToken authRequest = new JwtAuthenticationToken(authToken);
return getAuthenticationManager().authenticate(authRequest);
}
#Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
}
SecurityConfiguration
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(jwtAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/secured-resource-1/**", "/secured-resource-2/**")
.hasRole("ADMIN").antMatchers("/secured-resource-2/**").hasRole("ADMIN").and().formLogin()
.successHandler(new AuthenticationSuccessHandler()).and().httpBasic().and().exceptionHandling()
.accessDeniedHandler(new CustomAccessDeniedHandler()).authenticationEntryPoint(getBasicAuthEntryPoint())
.and()
.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()),
FilterSecurityInterceptor.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint() {
return new CustomBasicAuthenticationEntryPoint();
}
}
MainController
#RestController
public class MainController {
#Autowired
private JwtUtil jwtUtil;
#GetMapping("/secured-resource-1")
public String securedResource1() {
return "Secured resource1";
}
}
When I hit the endpoint with the valid JWT token, the code goes in a loop from Filter to provider class and ends in Error:
Exceeded maxRedirects. Probably stuck in a redirect loop http://localhost:8000/ error.
Debug logs shows the following error:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalStateException: Cannot call sendError() after the response has been committed] with root cause
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
Any suggestions what am I missing here.
Thanks in advance.
I believe the the reason for this is because you have not actually set the AuthenticationSuccessHandler for the bean JwtAuthenticationFilter, since it is not actually set it will keep looping around super and chain and later when the error needs to be sent since response is already written in super() chain.doFilter will fail because once the response is written it cannot be again written hence the error call sendError() after the response has been committed.
To correct this in your SecurityConfiguration before setting this
.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()),
FilterSecurityInterceptor.class)
Instantiate the filter and set it's success manager like so
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager()),FilterSecurityInterceptor.class);
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
Now use the above variable to set the filter.
This is a great reference project: https://gitlab.com/palmapps/jwt-spring-security-demo/-/tree/master/.
I solved this problem with another approach.
In the JwtAuthenticationFilter class we need to set authentication object in context and call chain.doFilter. Calling super.successfulAuthentication can be skipped as we have overridden the implementation.
#Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
//super.successfulAuthentication(request, response, chain, authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super("/**");
this.setAuthenticationManager(authenticationManager);
//this.setAuthenticationSuccessHandler(new JwtAuthenticationSuccessHandler());
}

How does Spring Security Filter Work With Custom Authentication and How To Combine It with Servlet Filter?

So I have a question regarding Spring Security. So I want to check authentication using custom header which then I want to check the token given in the custom header to redis value and set the data object as credentials at custom implementation of abstract authentication token.
I have already followed the tutorial in this web: https://shout.setfive.com/2015/11/02/spring-boot-authentication-with-custom-http-header/, but I can't update the authentication interface in SecurityContextHolder.getContext() (I set the credentials in my implementation of Authentication Interface, but when I get it in the service, the credentials is null).
I also found other problems, I actually want to order the filter like this:
ExceptionHandlerFilter (to catch exception error in the filter) -> Other filter or CustomWebSecurityConfigurerAdapter.
But when the url matches the antMatcher, I found that ExceptionHandlerFilter was skipped by the application.
I was so confused by this and could not find better tutorial in implementing custom authentication using Spring Security. So I want to ask whether you guys can tell me how Spring Security works and how to combine it with Filter?
Here is my first filter to catch exception
#Component
#Order(0)
public class ExceptionHandlerFilter extends OncePerRequestFilter {
private JaminExceptionHandler exceptionHandler;
private ObjectMapper objectMapper = new ObjectMapper();
#Autowired
public ExceptionHandlerFilter(JaminExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Throwable exception) {
ResponseEntity<?> responseEntity = this.exceptionHandler.handleException(exception, request);
response.setStatus(responseEntity.getStatusCode().value());
response.setHeader("Content-Type", "application/json");
response.getWriter().write(this.objectMapper.writeValueAsString(responseEntity.getBody()));
}
}
}
Here is my Auth Filter
#Component
public class AuthFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("J-Auth");
if (token != null) {
Authentication auth = new JaminAuthenticationToken(token);
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
} else {
throw new JaminException("Not authorized", JaminExceptionType.NOT_AUTHORIZED, HttpStatus.UNAUTHORIZED);
}
}
}
Authentication Provider
#Component
public class JaminAuthenticationProvider implements AuthenticationProvider {
private RedisTemplate<String, String> authRedis;
private ObjectMapper objectMapper = new ObjectMapper();
#Autowired
public JaminAuthenticationProvider(#Qualifier("authRedis") RedisTemplate<String, String> authRedis) {
this.authRedis = authRedis;
}
private UserDTO getUserDTO(String token) throws IOException {
String userData = this.authRedis.opsForValue().get(token);
if (userData == null) {
throw new JaminException("Not authorized", JaminExceptionType.NOT_AUTHORIZED, HttpStatus.UNAUTHORIZED);
}
return this.objectMapper.readValue(userData, UserDTO.class);
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JaminAuthenticationToken auth = (JaminAuthenticationToken) authentication;
try {
UserDTO userDTO = this.getUserDTO(auth.getToken());
auth.setCredentials(userDTO);
return auth;
} catch (IOException e) {
e.printStackTrace();
}
throw new JaminException("Not authorized", JaminExceptionType.NOT_AUTHORIZED, HttpStatus.UNAUTHORIZED);
}
#Override
public boolean supports(Class<?> authentication) {
return JaminAuthenticationToken.class.isAssignableFrom(authentication);
}
}
WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
#Order(1)
public class JaminSecurityAdapter extends WebSecurityConfigurerAdapter {
#Autowired
private JaminAuthenticationProvider jaminAuthenticationProvider;
private void disableDefaultSecurity(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();
http.formLogin().disable();
http.logout().disable();
http.httpBasic().disable();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
this.disableDefaultSecurity(http);
http.antMatcher("/auth/check")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(new AuthFilter(), BasicAuthenticationFilter.class);
// http.authorizeRequests().anyRequest().permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(jaminAuthenticationProvider);
}
}
Spring Security has some "before and after" steps. There are a few Handlers that can help. I don't know your code, but if you can get your authentication ok, maybe you just have to extend a SuccessHandler and set the authentication there, like i did in my blog project:
if(checkEmail(authentication)) {
val adminRole = SimpleGrantedAuthority("ROLE_ADMIN")
val oldAuthorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities()
val updateAuthorities = mutableListOf<GrantedAuthority>()
updateAuthorities.add(adminRole)
updateAuthorities.addAll(oldAuthorities)
SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken(authentication.getPrincipal(),
authentication.getCredentials(),
updateAuthorities))
}
And about the filters, maybe you can find your answer here. I don't like using filters and interceptors, but sometimes they are really necessary.

How to do basic authentication using cookies in spring security?

I am securing my REST api using Basic-Auth. On correct credentials passed by user, a controller is responsible for sending a httpOnly and secure cookie in response.
#GetMapping
#ResponseStatus(value=HttpStatus.OK)
public void loginUser( final HttpServletRequest request ,final HttpServletResponse response) throws UnsupportedEncodingException {
setAuthCookieToResonse(request,response);
}
private void setAuthCookieToResonse(final HttpServletRequest request ,final HttpServletResponse response) throws UnsupportedEncodingException {
String cookieKey = "auth";
String cookieValue = request.getHeader("Authorization");
if (cookieValue != null) {
Cookie cookie = new Cookie(cookieKey, cookieValue);
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
}
}
So, now with each request a cookie is being sent by the browser, which will contain Basic-Auth details. But the problem is, how do the spring security extract those credentials from that cookie?
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {// #formatter:off
httpSecurity
.cors()
.and().authorizeRequests()
.antMatchers("/signup/**").permitAll()
.anyRequest().authenticated()
.and().httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable()
;
}
My guess was:
To add a filter before BasicAuthenticationFilter.class and extract the credentials from cookie and than add those credentials to the HttpServletRequest's Authorizaton header which is going to be passed to spring-security layer. But the problem is, HttpServletRequest doesn't have API to add headers.
What would be the right way to implement this?
I made this working after following this blog (archived). But I would love to hear other solutions, especially using some spring configuration itself. Spring is a very matured framework, it must(should) have something to handle this common problem.
Since, the HttpServletRequest don't have any method to add the new headers, I need to create a custom class which can add new headers to the request, this can be achived by HttpServletRequestWrapper. Here is the implementation.
public final class MutableHttpServletRequest extends HttpServletRequestWrapper {
// holds custom header and value mapping
private final Map<String, String> customHeaders;
public MutableHttpServletRequest(HttpServletRequest request) {
super(request);
this.customHeaders = new HashMap<String, String>();
}
public void putHeader(String name, String value) {
this.customHeaders.put(name, value);
}
public String getHeader(String name) {
// check the custom headers first
String headerValue = customHeaders.get(name);
if (headerValue != null) {
return headerValue;
}
// else return from into the original wrapped object
return ((HttpServletRequest) getRequest()).getHeader(name);
}
public Enumeration<String> getHeaderNames() {
// create a set of the custom header names
Set<String> set = new HashSet<String>(customHeaders.keySet());
// now add the headers from the wrapped request object
Enumeration<String> e = ((HttpServletRequest) getRequest()).getHeaderNames();
while (e.hasMoreElements()) {
// add the names of the request headers into the list
String n = e.nextElement();
set.add(n);
}
// create an enumeration from the set and return
return Collections.enumeration(set);
}
}
The filter which checks for the cookie, before the Spring-secuirty:
public class CheckAuthCookieFilter implements Filter {
private Logger logger = LoggerFactory.getLogger(getClass());
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
MutableHttpServletRequest mutableRequest = new MutableHttpServletRequest(httpServletRequest);
Cookie[] cookies = httpServletRequest.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
logger.debug(cookie.getName() + " : " + cookie.getValue());
if (cookie.getName().equalsIgnoreCase("auth")) {
mutableRequest.putHeader("Authorization", URLDecoder.decode(cookie.getValue(), "utf-8"));
}
}
}
chain.doFilter(mutableRequest, response);
}
}
and finally the security configuration:
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {// #formatter:off
httpSecurity
.cors()
.and().authorizeRequests()
.antMatchers("/signup/**").permitAll()
.anyRequest().authenticated()
.and().httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable()
;
httpSecurity.addFilterBefore(new CheckAuthCookieFilter(), BasicAuthenticationFilter.class);
}
My custom filter will run before the Spring's BasicAuthenticationFilter.If there is a cookie present with name auth(which the application created on successful login), than that's the cookie which holds the basic auth credentials. The credentials are extracted from that, and added to the header of request. Then the BasicAuthenticationFilter will run and look for the Authorization and proceed with its normal flow.

Set response header in Spring Boot

How can I set the response header for each call in my application made with Spring Boot?
I would like to try to use a filter to intercept all the calls and be able to set the response header.
I followed the guide Disable browser caching HTML5, but only set the request header, and not always.
There are three ways to do this:
Set the response for a specific controller, in the Controller class:
#Controller
#RequestMapping(value = DEFAULT_ADMIN_URL + "/xxx/")
public class XxxController
....
#ModelAttribute
public void setResponseHeader(HttpServletResponse response) {
response.setHeader("Cache-Control", "no-cache");
....
}
or
#RequestMapping(value = "/find/employer/{employerId}", method = RequestMethod.GET)
public List getEmployees(#PathVariable("employerId") Long employerId, final HttpServletResponse response) {
response.setHeader("Cache-Control", "no-cache");
return employeeService.findEmployeesForEmployer(employerId);
}
Or you can put the response header for each call in the application (this is for Spring annotation-based, otherwise see automatically add header to every response):
#Component
public class Filter extends OncePerRequestFilter {
....
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//response.addHeader("Access-Control-Allow-Origin", "*");
//response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
response.setHeader("Cache-Control", "no-store"); // HTTP 1.1.
response.setHeader("Pragma", "no-cache"); // HTTP 1.0.
response.setHeader("Expires", "0"); // Proxies.
filterChain.doFilter(request, response);
}
}
The last way I found is using an Interceptor that extends HandlerInterceptorAdapter; for more info see https://www.concretepage.com/spring/spring-mvc/spring-handlerinterceptor-annotation-example-webmvcconfigureradapter
create your Interceptor that extends HandlerInterceptorAdapter:
public class HeaderInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) {
httpServletResponse.setHeader("Cache-Control", "no-store"); // HTTP 1.1.
httpServletResponse.setHeader("Pragma", "no-cache"); // HTTP 1.0.
httpServletResponse.setHeader("Expires", "0"); // Proxies.
return true;
}
}
In your MvcConfig thath extends WebMvcConfigurerAdapter you must Override the addInterceptors method and add new Interceptor:
#Override
public void addInterceptors(InterceptorRegistry registry) {
....
registry.addInterceptor(new HeaderInterceptor());
}
I hope I was helpful!
Implement Filter and is registered by #Component annotation. The #Order(Ordered.HIGHEST_PRECEDENCE) is used for Advice execution precedence.
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class NoCacheWebFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(NoCacheWebFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.debug("Initiating WebFilter >> ");
}
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HeaderMapRequestWrapper requestWrapper = new
HeaderMapRequestWrapper(req);
// implement you logic to add header
//requestWrapper.addHeader("remote_addr", "");
chain.doFilter(requestWrapper, response);
}
#Override
public void destroy() {
logger.debug("Destroying WebFilter >> ");
}
}

Security filters are overlapping

When multiple filters are added to the HttpSecurity configure method, they seem to be overlapping because only one works at the time.
This is the configure method:
#Override
public void configure(HttpSecurity http) throws Exception {
http
.logout().and().antMatcher("/**")
.addFilterBefore(ssoFilter(), RequestHeaderAuthenticationFilter.class)
.authenticationProvider(preauthAuthProvider())
.authorizeRequests()
.antMatchers("/index.html", "/home.html", "/", "/login").permitAll()
.anyRequest().authenticated().and().csrf()
.csrfTokenRepository(csrfTokenRepository()).and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}
I've tried to specify the order but the issue still persists:
#Bean
public FilterRegistrationBean securityFilterChain(#Qualifier(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) Filter securityFilter) {
FilterRegistrationBean registration = new FilterRegistrationBean(securityFilter);
registration.setOrder(Integer.MAX_VALUE - 2);
registration.setName(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
return registration;
}
#Bean
public FilterRegistrationBean ssoFilterRegistrationBean() throws Exception {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(ssoFilter());
registrationBean.setOrder(Integer.MAX_VALUE-1);
return registrationBean;
}
#Bean
public FilterRegistrationBean csrfFilterRegistrationBean() throws Exception {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(csrfHeaderFilter());
registrationBean.setOrder(Integer.MAX_VALUE);
return registrationBean;
}
I've followed the following thread with no success.
Filter order in spring-boot
https://github.com/spring-projects/spring-boot/issues/1640
https://github.com/spring-projects/spring-boot/issues/677
Any help will be appreciated!
UPDATE:
CSRF Filter definition
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request
.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null
|| token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
SSO Filter definition:
public class SSORequestHeaderAuthenticationFilter extends RequestHeaderAuthenticationFilter {
private boolean allowPreAuthenticatedPrincipals = true;
public SSORequestHeaderAuthenticationFilter() {
super();
//TODO Pull this value from a properties file (application.properties, or localstrings.properties)
//NOTE SM_USER is the default, but you can change it like this (your company may use some other header)
//this.setPrincipalRequestHeader("SM_USER");
}
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
}
/**
* This is called when a request is made, the returned object identifies the
* user and will either be {#literal null} or a String. This method will throw an exception if
* exceptionIfHeaderMissing is set to true (default) and the required header is missing.
*
* #param request {#link javax.servlet.http.HttpServletRequest}
*/
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String userName = (String) (super.getPreAuthenticatedPrincipal(request));
if (userName == null || userName.trim().equals("")) {
return userName;
}
return userName;
}
public boolean isAllowPreAuthenticatedPrincipals() {
return allowPreAuthenticatedPrincipals;
}
}
My guess is that you are not always executing FilterChain.doFilter method inside both filters. Then the filter chain stops and only one of your custom filters is executed. In this simple example both filters executed:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(new Filter1(), RequestHeaderAuthenticationFilter.class)
.addFilterAfter(new Filter2(), CsrfFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.logout()
.permitAll();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
static class Filter1 extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("executed filter 1");
filterChain.doFilter(request, response);
}
}
static class Filter2 extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("executed filter 2");
filterChain.doFilter(request, response);
}
}
}
You are confusing container registration (with FilterRegistrationBean) and registration in a Security filter chain (with HttpSecurity) and also possibly with the order of the filter chains within Spring Security. If a given filter chain is selected by Spring Security all the filters in it are not even necessarily fired anyway (filters can always switch off other downstream filters).
I suggest you stop worrying about the order in your FilterRegistrationBeans and use them to disable the container registration (by setting their enabled flag to false). Then think about the order of your filter chains, as specified by the #Order on your WebSecurityConfigurers. And finally you can decide if the order of the filters in a given chain matters, and if it does use the addFilter{Before,After} methods.
A Filter which does what you want to do does already exist. Its the OAuth2AuthenticationProcessingFilter.
This filter is used if you annotate your application with #EnableResourceServer. If you do so this will cause, that only token based authentication will work now.
You have to set the stateless flag of this filter to false to allow other ways of authentication, too.
What i did is to create a class ApiTokenAccessFilter which extends OAuth2AuthenticationProcessingFilter. This filter takes a ResourceServerTokenServices constructor parameter and sets the stateless flag to false.
public class ApiTokenAccessFilter extends OAuth2AuthenticationProcessingFilter {
public ApiTokenAccessFilter(ResourceServerTokenServices resourceServerTokenServices) {
super();
setStateless(false);
setAuthenticationManager(oauthAuthenticationManager(resourceServerTokenServices));
}
private AuthenticationManager oauthAuthenticationManager(ResourceServerTokenServices tokenServices) {
OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
oauthAuthenticationManager.setResourceId("oauth2-resource");
oauthAuthenticationManager.setTokenServices(tokenServices);
oauthAuthenticationManager.setClientDetailsService(null);
return oauthAuthenticationManager;
}
}
In my security config i used this Filter as follows:
#Configuration
#EnableOAuth2Sso
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private ResourceServerTokenServices tokenServices;
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.addFilterBefore(new ApiTokenAccessFilter(tokenServices), AbstractPreAuthenticatedProcessingFilter.class);
}
}
I think this could be easier so i opened an issue on the spring-security-oauth Github repo. I'm not sure whether this solution is the way to go, but i didn't find another alternative.

Resources