Search code examples
springspring-mvcspring-securityspring-bootspring-oauth2

HttpSession null after replacing AuthorizationRequest


The problem

The HttpSession becomes null after a custom implementation of DefaultOAuth2RequestFactory replaces the current AuthorizationRequest with a saved AuthorizationRequest. This causes failure of the subsequent request to /oauth/token because the CsrfFilter in the Spring Security filter chain preceding the /oauth/token endpoint is not able to find a session Csrf token in the null session to compare with the request's Csrf token.

Control flow during the error

The following flowchart illustrates where Step 14 and Step 15 somehow null-ify the HttpSession. (Or possibly mismatch a JSESSIONID.) A SYSO at the start of CustomOAuth2RequestFactory.java in Step 14 shows that there is indeed an HttpSession that does in fact contain the correct CsrfToken. Yet, somehow, the HttpSession has become null by the time Step 15 triggers a call from the client at the localhost:8080/login url back to the localhost:9999/oauth/token endpoint.

Breakpoints were added to every line of the HttpSessionSecurityContextRepository mentioned in the debug logs below. (It is located in the Maven Dependencies folder of the authserver eclipse project.) These breakpoints confirmed that the HttpSession is null when the final request to /oauth/token is made in the flowchart below. (Bottom-left of flowchart.) The null HttpSession might be due to the JSESSIONID that remains in the browser becoming out of date after the custom DefaultOAuth2RequestFactory code runs.

How can this problem be fixed, so that the same HttpSession remains during the final call to the /oauth/token endpoint, after the end of Step 15 in the flowchart?

Relevant code and logs

We can guess that the null session is due to either 1.) the JSESSIONID not being updated in the browser by the code in the CustomOAuth2RequestFactory, or 2.) the HttpSession actually being null-ified.

The Spring Boot debug logs for the call to /oauth/token after Step 15 clearly state that there is no HttpSession by that point, and can be read as follows:

2016-05-30 15:33:42.630 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy        : /oauth/token at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy        : /oauth/token at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy        : /oauth/token at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@2fe29f4b
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy        : /oauth/token at position 4 of 12 in additional filter chain; firing Filter: 'CsrfFilter'
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:9999/uaa/oauth/token
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed

Recreating the problem on your computer

You can recreate the problem on any computer by following these simple steps:

  1. Download the zipped version of the app (editor's note: unfortunately this is no longer available).

  2. Unzip the app by typing: tar -zxvf oauth2.tar(4).gz

  3. Launch the authserver app by navigating to oauth2/authserver and then typing mvn spring-boot:run.

  4. Launch the resource app by navigating to oauth2/resource and then typing mvn spring-boot:run

  5. Launch the ui app by navigating to oauth2/ui and then typing mvn spring-boot:run

  6. Open a web browser and navigate to http : // localhost : 8080

  7. Click Login and then enter Frodo as the user and MyRing as the password, and click to submit.

  8. Enter 5309 as the Pin Code and click submit. This will trigger the error shown above.

The Spring Boot debug logs will show A LOT of SYSO, which gives the values of variables such as XSRF-TOKEN and HttpSession at each step shown in the flowchart. The SYSO helps segment the debug logs so that they are easier to interpret. And all the SYSO is done by one class called by the other classes, so you can manipulate the SYSO-generating class to change reporting everywhere in the control flow. The name of the SYSO-generating class is TestHTTP, and its source code can be found in the same demo package.

Use the debugger

  1. Select the terminal window that is running the authserver app and type Ctrl-C to stop the authserver app.

  2. Import the three apps (authserver, resource, and ui) into eclipse as existing maven projects.

  3. In the authserver app's eclipse Project Explorer, click to expand the Maven Dependencies folder, then scroll down within it to click to expand the Spring-Security-web... jar as shown circled in orange in the image below. Then scroll to find and expand the org.springframework.security.web.context package. Then double click to open the HttpSessionSecurityContextRepository class highlighted in blue in the screen shot below. Add breakpoints to every line in this class. You may want to do the same to the SecurityContextPersistenceFilter class in the same package. These breakpoints will enable you to see the value of the HttpSession, which currently becomesnull before the end of the control flow, but needs to have a valid value that can be mapped to an XSRF-TOKEN in order to resolve this OP.

  4. In the app's demo package, add breakpoints inside the CustomOAuth2RequestFactory.java. Then Debug As... Spring Boot App to start the debugger.

  5. Then repeat steps 6 through 8 above. You may want to clear the browser's cache before each new attempt. And you may want the Network tab of the browser's developer tools open.


Solution

  • Have you solved your issue? I have been looking around to find a full sample of 2FA together with spring-security-oauth2. It is great that you have posted your full concepts and the complete sources.

    I tried your package and your issue can simply be resolved by changing just 1 line of code in your AuthserverApplication.java

    @Override
        protected void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http
                .formLogin().loginPage("/login").permitAll()
            .and()
                    .requestMatchers().antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication", "/pincode")
            .and()
                    .authorizeRequests().anyRequest().authenticated();
            // @formatter:on
        }
    

    Your original configuration by passed the authentication chain of spring security which returned you a null object of authentication.

    I would also recommend you to change the Bean creation of CustomOAuth2RequestFactory to the following which override all the OAuth2RequestFactory in the chain

    @Bean
        public OAuth2RequestFactory customOAuth2RequestFactory(){
            return new CustomOAuth2RequestFactory(clientDetailsService);
        }
    

    For the code you have added for handling the CSRF, you may just simply remove them, eg. the 2FA controller:

    @Controller
    @RequestMapping(TwoFactorAuthenticationController.PATH)
    public class TwoFactorAuthenticationController {
        private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
        public static final String PATH = "/secure/two_factor_authentication";
        public static final String AUTHORIZE_PATH = "/oauth/authorize";
        public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
    
        private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    
        @RequestMapping(method = RequestMethod.GET)
        public String auth(HttpServletRequest request, HttpSession session, HttpServletResponse resp/*, ....*/) {
            System.out.println("-------- inside GET /secure/two_factor_authentication --------------");
            if (AuthenticationUtil.isAuthenticatedWithAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
                LOG.info("User {} already has {} authority - no need to enter code again", ROLE_TWO_FACTOR_AUTHENTICATED);
    //            throw ....;
            }
            else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
    //            LOG.warn("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
    //          throw ....;
            }
            return "pinCode";
        }
    
        @RequestMapping(method = RequestMethod.POST)
        public String auth(FormData formData, HttpServletRequest req, HttpServletResponse resp,
                                                SessionStatus sessionStatus, Principal principal, Model model)
            throws IOException{
    
            if (formData.getPinVal()!=null) {
                if(formData.getPinVal().equals("5309")){
                    AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);
                    return "redirect:"+AUTHORIZE_PATH;
                };
            };
    
            return "pinCode";
        }
    }
    

    Please kindly let me know if you want a complete source codes after cleanup.