Search code examples
javaspring-bootspring-securityoauth-2.0spring-authorization-server

Upgrading to Spring Security 6 401's - sessions issue? - updating to SAS 1.0.1


I'm finally getting around to updating some old code from Spring Authorization Server 0.3.0 to 1.0.1. (which of course means updating to spring security 6.x)

I have a custom flow which requires an initial JWT token to be validated before entering the main oauth flow ( from my older question and useful answer here: https://stackoverflow.com/a/69577014/2399972 ). This currently has the configuration as follows, which works perfectly as of 0.3.0 and SS 5.

This is my configuration. My understanding is that the first one is for the oauth endpoints, to allow them to accept sessions created in other requests?

The second chain is for my custom /login endpoint, which will pull off a bearer token, validate it and then redirect to the /oauth/login..... (as shown in the code block after this one).

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(
                        authorize ->
                                authorize
                                        .antMatchers(publicRoutes.toArray(new String[0]))
                                        .permitAll()
                                        .anyRequest()
                                        .authenticated())
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        http.csrf().disable();
        http.headers().frameOptions().disable();

        return http.build();
    }

this allows me to consume a bearer token on a /login endpoint on the same service, validate it, and then pass the flow to /oauth2/authorize - like so:

    @GetMapping("/login")
    public String login(
            @RequestParam("response_type") String responseType,
            @RequestParam("client_id") String clientId,
            @RequestParam("scope") String scope,
            @RequestParam("state") String state,
            @RequestParam("redirect_uri") String redirectUri,
            @RequestParam("nonce") String nonce,
            Authentication authentication,
            HttpServletRequest httpServletRequest,
            HttpSession httpSession) {
       
        return String.format(
                "redirect:%s/oauth2/authorize?response_type=%s&client_id=%s&scope=%s&state=%s&nonce=%s&redirect_uri=%s",
                serverFqdn, responseType, clientId, scope, state, nonce, redirectUri);
    }

So - this all works on 0.3.0 and spring security 5.

On updating to 1.0.1 I get a 401 when the /oauth2/authorize is called that I can't figure out.

I'm not 100% but I've read that the sessions are persisted differently in Spring Security 6, so now need to explicitly save the session - or something? Here's my attempt at this:

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer( OAuth2AuthorizationServerConfigurer.class)
                .oidc( Customizer.withDefaults());
        http
        .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.ALWAYS )
        .and()
        .securityContext(securityContext -> securityContext
            .requireExplicitSave( false )
            .securityContextRepository( patchedDelegatingSecurityContextRepository )
// or can this be any securityContextRepository that's here.. I assumed it needs to be a bean?
        );
        return http.build();
    }

And on the jwt secured /login redir to oauth - it's the same as before, but now I'm trying to save the session.

   @GetMapping("/login")
    public String login(
            @RequestParam("response_type") String responseType,
            @RequestParam("client_id") String clientId,
            @RequestParam("scope") String scope,
            @RequestParam("state") String state,
            @RequestParam("redirect_uri") String redirectUri,
            @RequestParam("nonce") String nonce,
            Authentication authentication,
            HttpServletRequest httpServletRequest,
            HttpSession httpSession) {
       
      SecurityContext context = securityContextHolderStrategy.getContext();
      context.setAuthentication(authentication);
      SecurityContextHolder.setContext(context);
      securityContextHolderStrategy.setContext( context );
      sessionSecurityContextRepository.saveContext(context, httpServletRequest,response);

        return String.format(
                "redirect:%s/oauth2/authorize?response_type=%s&client_id=%s&scope=%s&state=%s&nonce=%s&redirect_uri=%s",
                serverFqdn, responseType, clientId, scope, state, nonce, redirectUri);
    }

Despite this, the session doesn't seem to be saved, not retrieved correctly - I think?

Or is there something else I'm totally missing here? All help and info very welcome.


Solution

  • In Spring Security 6, the SecurityContext is indeed no longer automatically saved for every request (depending on the sessionManagement() config).

    The behavior of saving the SecurityContext is now the responsibility of each individual authentication mechanism (usually a filter, such as UsernamePasswordAuthenticationFilter). When you are customizing authentication, you will usually need to handle saving the context yourself, as you are doing now.

    The issue is that the type of Authentication in your case is JwtAuthenticationToken, which is marked as @Transient. This means it will not be saved even if you call securityContextRepository.saveContext(...). I'm not clear how this was working in 0.3.0 (which used spring-security 5.7.x and included this bug fix). However, you can work around this by using another Authentication type that is non-@Transient, for example:

    @Controller
    public class SsoController {
        private final SecurityContextHolderStrategy securityContextHolderStrategy =
                SecurityContextHolder.getContextHolderStrategy();
    
        private final SecurityContextRepository securityContextRepository =
                new HttpSessionSecurityContextRepository();
    
        @GetMapping("/login")
        public String login(
                @RequestParam("response_type") String responseType,
                @RequestParam("client_id") String clientId,
                @RequestParam("scope") String scope,
                @RequestParam("state") String state,
                @RequestParam("redirect_uri") String redirectUri,
                @RequestParam("nonce") String nonce,
                @AuthenticationPrincipal Jwt jwt,
                HttpServletRequest request,
                HttpServletResponse response) {
    
            List<GrantedAuthority> authorities =
                    AuthorityUtils.createAuthorityList("USER");
            User user = new User(jwt.getSubject(), null, authorities);
            UsernamePasswordAuthenticationToken authenticationResult =
                    UsernamePasswordAuthenticationToken.authenticated(
                            user, null, authorities);
    
            SecurityContext securityContext = this.securityContextHolderStrategy.createEmptyContext();
            securityContext.setAuthentication(authenticationResult);
            this.securityContextHolderStrategy.setContext(securityContext);
            this.securityContextRepository.saveContext(securityContext, request, response);
    
            return String.format("redirect:/oauth2/authorize?response_type=%s&client_id=%s&scope=%s&state=%s&redirect_uri=%s&nonce=%s",
                    responseType, clientId, scope, state, redirectUri, nonce
            );
        }
    }
    

    (I didn't include your example with serverFqdn verbatim as I don't know where that variable is coming from. Feel free to change this example as necessary to meet your needs.)

    Notice the pattern of creating an empty SecurityContext and populating it, and then setting it afterwards. In other words, you should treat the existing SecurityContext as immutable even though it isn't.

    Also note that setting the SecurityContext in this @Controller is actually optional (since you don't need to do any additional processing in this request), only saving the context is required at this stage. I included it to complete the example.