Search code examples
springauthenticationspring-securityspring-boot

Spring Security Custom filter creates infinite loop


dependencies: Spring Boot 1.1.5.RELEASE, Spring 4.0.6.RELEASE, Spring Security 3.2.4.RELEASE

problem: I am creating a custom filter that based on a token in the request queries a provider to authenticate the user. Custom filter code and config are here. As you can see I have disabled as much as possible to pinpoint the issue. Enabling debug shows me

Security filter chain: [
  WebAsyncManagerIntegrationFilter
  CustomAuthenticationFilter
  SecurityContextPersistenceFilter
  SecurityContextHolderAwareRequestFilter
  FilterSecurityInterceptor
]

Another thing is that the logs tell me the user was authenticated.

When querying the app an infinite loop happens and eventually the request returns, curl says it cannot follow that many redirects, postman says there is something wrong with the server. I have read some posts about infinite loops because login page is secured. In my case I dont want to enable any login in my app. If I cannot authenticate the user then 401/403 status will be returned. Have in mind that the app acts as a authentication proxy for client requests but it is not a client itself. I am asking for help in either pinpointing the error in the current configuration, or to confirm that my approach does not work? Either way, thank you in advance for your help.

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig
    extends WebSecurityConfigurerAdapter {
    @Value("${oauth.check_token.url:http://localhost:3030/oauth/token/info}")
    private String checkTokenUrl;

    @Autowired @Qualifier("restTemplate")
    private RestOperations authRestTemplate;

[EDIT] I found out infinite loops stopped if I dont use postman, and if I dont tell curl to follow redirects. Below is the full log I get if I place CustomAuthenticationFilter before SecurityContextPersistentFilter.

************************************************************

    Request received for GET '/test-oauth':

    org.apache.catalina.connector.RequestFacade@35a0339c

    servletPath:/test-oauth
    pathInfo:null
    headers: 
    user-agent: curl/7.30.0
    host: localhost:8081
    accept: */*
    authorization: Bearer 8f58520f137b25b096b48a67135c5b9b294892a8c712d5c2bcb8d90ab9f6efd0


    Security filter chain: [
      WebAsyncManagerIntegrationFilter
      CustomAuthenticationFilter
      SecurityContextPersistenceFilter
      SecurityContextHolderAwareRequestFilter
      FilterSecurityInterceptor
    ]


    ************************************************************


    2014-08-23 18:54:39.453 DEBUG --- [nio-8081-exec-1] CustomAuthenticationFilter : Request is to process authentication
    2014-08-23 18:54:39.453 DEBUG --- [nio-8081-exec-1] CustomAuthenticationFilter : Attempting to authenticate: Bearer 8f58520f137b25b096b48a67135c5b9b294892a8c712d5c2bcb8d90ab9f6efd0
    2014-08-23 18:54:39.569 DEBUG --- [nio-8081-exec-1] CustomAuthenticationFilter : Authentication success. Updating SecurityContextHolder to contain: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@93e3eff0: Principal: com.shift.sysops.auth.AuthSSOUser@f02988d6: Username: username; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER

If I placed the custom filter in any other position it throws a NPE because SecurityContextRepository is null in SecurityContextPersistenceFilter.

2014-08-23 18:41:50.060 ERROR --- [nio-8081-exec-1] [dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

java.lang.NullPointerException: null
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:82)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.springframework.security.web.debug.DebugFilter.invokeWithWrappedRequest(DebugFilter.java:70)
    at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:59)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.springframework.boot.actuate.autoconfigure.MetricFilterAutoConfiguration$MetricsFilter.doFilterInternal(MetricFilterAutoConfiguration.java:89)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
    at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:683)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1040)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1720)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)

Solution

  • As I understand you have the following use case:

    1. user is authenticated with some third party app (i.e. not your app)

    2. Once user access your app, you get the auth header and check whether the user is authenticated or not.

    3. If he is authenticated, you fetch his permissions, details etc and allow him to access your application
    4. if he is not authenticated, you send 401 error response.

    For this use case, its best to extend AbstractPreAuthenticatedProcessingFilter.

    1. You define a custom pre-auth filter by extending AbstractPreAuthenticatedProcessingFilter.
    2. In your custom filter, you need to override two methods getPreAuthenticatedPrincipal() and getPreAuthenticatedCredentials()
    3. In getPreAuthenticatedPrincipal(), you can check whether an auth header exists in request, if it exists return the header name in as principal and header value in credentials
    4. Use PreAuthenticatedAuthenticationProvider and provide your custom preAuthenticatedUserDetailsService (By extending AuthenticationUserDetailsService) to check if auth token is valid, if its valid also fetch granted authorities else throw AuthenticationException like BadCredentialsException
    5. Create your custom AuthenticationEntryPoint implementing AuthenticationEntryPoint, and override commence method to return 401 response.

    In case valid auth header exists, pre-auth filter will set authenticated user in springSecurityContext, if auth header is missing/invalid 402 error response will be returned.

    Hope it helps.