Skip to content

OIDC Authorization Code Grant Type#

  • In the Keycloak Setup, we learned how to set up Keycloak authorization server for OIDC Authorization Code Grant Type . Now, in this section, we will continue to configure the Spring Boot application to be as Client and also Resource Server for connecting to the Keycloak to get token and also verifying in coming token requests with before accessing the data.

Prepare#

  • Firstly, we need to review the simple diagram that we are going to do as in the image below.

 #zoom

  1. The user access Browser and then call the API /v1/secure/messages for getting the protected data.
  2. The Spring Boot application checks the credential and realize that, the user has not logged in yet then the it will save this API URL into a request cache currently the API URL is stored under a JESSIONID and it will be used in steps later. Then the user will be responded with a redirect URL to trigger authorization flow.
  3. The Browser calls the the responded API for triggering the authorization flow.
  4. When the API /oauth2/authorization/keycloak is called then the Spring Boot application will prepare an authorization redirect URL and it will also save the OAuth2AuthorizationRequest under the JESSIONID on the step above. Then the authorization redirect URL will be responded to the browser.
  5. The Browser will redirect to the responded URL to the Keycloak Authorization Server page for login with username and password to prove the user identity.
  6. If the username and password are correct then the Keycloak Authorization Server will respond a redirect URL together with an authorization code to the Browser. This redirect URL is the client registered redirect that we configured in Keycloak Authorization Server in Keycloak Setup.
  7. The Browser call the client registered redirect URL together with the Authorization Code and JESSIONID
  8. When the URL is called the filter OAuth2LoginAuthenticationFilter of Spring Boot application will be triggered and it will load the OAuth2AuthorizationRequest which is stored in the session to extract some information and repair the request with the authorization code and call to the Keycloak Authorization Server to verify the auhthorization code and exchange the Access Token and ID token if the authorization code is correct.
  9. If the authorization code is correct then the Access Token, ID Token and other information will be responded.
  10. The Spring Boot application will extract those information and build the OAuth2AuthenticationToken. Then this Authentication will set into a new session and this session will be responded as a new JSESSIONID cookie. This Authentication also be set into the Spring Security Context. Finally, the JESSIONID cookie with the redirect URL which is the first URL that the browser called /v1/secure/messages without the authorization will be responded to the Browser, this redirect URL is got from the request cache at step 2.
  11. When Browser received the JESSIONID cookie and redirect URL. Then it will call the redirect URL and the Browser will also set the JESSIONID into the request automatically.
  12. The Spring Boot application received the request with the JESSIONID (processed in the SecurityContextHolderFilter) and it look up the corresponding session in the session store by the value of JSESSIONID. If the session exists and the SecurityContext in the session is valid also then the SecurityContext is set in the SecurityContextHolder and finally the data will be response to the Browser.

  13. For setting up Keycloak authorization server please view Keycloak Setup with Client Credentials Grant Type.

Spring Boot Server Setup#

Dependencies#

  • Let's add some dependencies as below.
pom.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<dependencies>
    <!-- spring boot 3 web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.1.3</version>
    </dependency>

    <!-- spring boot 3 test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>3.1.3</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
        <version>3.1.3</version>
    </dependency>

    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.28</version>
        <scope>provided</scope>
    </dependency>

    <!-- Apache commons-lang3 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.13.0</version>
    </dependency>

    <!-- slf4j -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.0.9</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.9</version>
        <scope>test</scope>
    </dependency>

</dependencies>
  • In which for setting up OIDC Authorization Code Grant Type with Spring Boot we need the dependency spring-boot-starter-oauth2-client.

Controller#

  • Then let's create 2 simple controllers with 2 simple APIs for testing. One controller is public and the other one is secure controller which need the authentication to access.
PublicController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.springboot.project.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PublicController {

    @RequestMapping(method = RequestMethod.GET, path = "/v1/public/messages")
    public ResponseEntity<String> getMessage() {
        return ResponseEntity.ok("Public Message!");
    }

}
SecuredController.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.springboot.project.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SecuredController {

    @RequestMapping(method = RequestMethod.GET, path = "/v1/secure/messages")
    public ResponseEntity<String> getMessage() {
        return ResponseEntity.ok("Secured Message!");
    }

}

Config#

  • Next, let's create a SecurityConfig.java for configuring the security as below.
SecurityConfig.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.springboot.project.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth ->
                    auth.requestMatchers("/v1/public/**")
                            .permitAll()
                            .anyRequest()
                            .authenticated())
                .oauth2Login(withDefaults());
        return http.build();
    }

}
  • As you can see that, in this config:

    • We just simply disable the csrf by using http.csrf(AbstractHttpConfigurer::disable).
    • Then we config public APIs which don't need the authorization by
    1
    2
    3
    4
    5
    .authorizeHttpRequests(auth ->
                        auth.requestMatchers("/v1/public/**")
                                .permitAll()
                                .anyRequest()
                                .authenticated())
    
    • Finally, we will configure authentication for OIDC by using .oauth2Login(withDefaults());
  • To use the built-in default OIDC configuration we have to provide some client information in the application.yaml as below.

application.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
server:
  port: 7070
spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            provider: keycloak
            client-id: auth-code-grant-type
            client-secret: QXHFcSiFLF96KassAQppb9HJtZ45Lbbf
            authorization-grant-type: authorization_code
            scope: openid, profile, email
            redirect-uri: "http://localhost:7070/login/oauth2/code/keycloak"
        provider:
          keycloak:
            issuer-uri: http://localhost:8080/auth/realms/myrealm
            authorization-uri: http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/auth
            token-uri: http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/token
            user-info-uri: http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/userinfo
            jwk-set-uri: http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/certs
  • So we have the definitions for those configurations as below:

  • Spring Security OAuth2 Client Configuration

    • spring.security.oauth2.client.registration.keycloak: Defines the client registration details for Keycloak.
      • provider: Specifies the provider for the OAuth2 client, which is Keycloak.
      • client-id: The client ID registered with Keycloak.
      • client-secret: The client secret associated with the client ID.
      • authorization-grant-type: The type of authorization grant to be used, here it is authorization_code.
      • scope: The scope of the access request (e.g., openid, profile, email).
      • redirect-uri: The URI where the authorization server will redirect to with the authorization code.
    • spring.security.oauth2.client.provider.keycloak: Defines the provider details for Keycloak.
      • issuer-uri: The issuer URI for the Keycloak realm.
      • authorization-uri: The URI to initiate the authorization request.
      • token-uri: The URI to obtain the access token.
      • user-info-uri: The URI to obtain user information.
      • jwk-set-uri: The URI to obtain the JSON Web Key Set (JWKS) for verifying tokens.
Configuration Value Description
server.port 7070 The port number on which the server runs.
spring.security.oauth2.client.registration.keycloak.provider keycloak The OAuth2 provider name.
spring.security.oauth2.client.registration.keycloak.client-id auth-code-grant-type The client ID registered with Keycloak.
spring.security.oauth2.client.registration.keycloak.client-secret QXHFcSiFLF96KassAQppb9HJtZ45Lbbf The client secret associated with the client ID.
spring.security.oauth2.client.registration.keycloak.authorization-grant-type authorization_code The type of authorization grant.
spring.security.oauth2.client.registration.keycloak.scope openid, profile, email The scope of the access request.
spring.security.oauth2.client.registration.keycloak.redirect-uri http://localhost:7070/login/oauth2/code/keycloak The URI where the authorization server will redirect with the authorization code.
spring.security.oauth2.client.provider.keycloak.issuer-uri http://localhost:8080/auth/realms/myrealm The issuer URI for the Keycloak realm.
spring.security.oauth2.client.provider.keycloak.authorization-uri http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/auth The URI to initiate the authorization request.
spring.security.oauth2.client.provider.keycloak.token-uri http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/token The URI to obtain the access token.
spring.security.oauth2.client.provider.keycloak.user-info-uri http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/userinfo The URI to obtain user information.
spring.security.oauth2.client.provider.keycloak.jwk-set-uri http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/certs The URI to obtain the JSON Web Key Set (JWKS) for verifying tokens.

Testing#

  • Now, let's start the Spring Boot application, open browser and go to http://localhost:7070/v1/public/messages then we can see the message immediately without login.

 #zoom

  • Then let's go to http://localhost:7070/v1/secure/messages then you will be navigated to the Keycloak login page for authentication as below.

 #zoom

  • Then after login successfully we can see the secure message as below.

 #zoom

Deep Dive Into Details#

  • After we testing successfully, now let's open the network and open debug mode for Spring Boot application, and we will check step by step the OIDC Authorization Code Grant Type flow in Spring Security.

 #zoom

  • So for the first step, when we access the protected API http://localhost:7070/v1/secure/messages. Then the Spring Boot application checks the credential and realize that, the user has not logged in yet then the it will save this API URL into a request cache, currently the request cache will be stored under a JESSIONID (E6730EA2590274BC5EE3AC11472B29F2) and it will be used in steps later. Then the user will be responded with a redirect URL http://localhost:7070/oauth2/authorization/keycloak to trigger authorization flow.

 #zoom

  • Now, The Browser calls the the responded API for triggering the authorization flow. When the API /oauth2/authorization/keycloak is called then the Spring Boot application will prepare an authorization redirect URL and it will also save the OAuth2AuthorizationRequest into a session (the JESSIONID - E6730EA2590274BC5EE3AC11472B29F2 that we saw in the step above). Then the authorization redirect URL will be responded to the browser. For example: http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/auth?response_type=code&client_id=auth-code-grant-type&scope=openid%20profile%20email&state=Ho4AIPgBfVSA8tykxnsfklRUQo2mRL7cOX5LWe_ex_8%3D&redirect_uri=http://localhost:7070/login/oauth2/code/keycloak&nonce=gugi6sOh-muBFRb9JZFvd0D6Qkmz83MGpEPHk1ESsP4

 #zoom

  • The Browser will redirect to the responded URL to the Keycloak Authorization Server page for login with username and password to prove the user identity.

 #zoom

  • After the user click button login then the API in the image above will be called and if the username and password are correct then the Keycloak Authorization Server will respond a redirect URL together with an authorization code to the Browser. This redirect URL is the client registered redirect that we configured in Keycloak Authorization Server in Keycloak Setup.

 #zoom

  • The Browser call the client registered redirect URL together with the Authorization Code and JESSIONID. When the URL is called the filter OAuth2LoginAuthenticationFilter which extends the AbstractAuthenticationProcessingFilter of Spring Boot application will be triggered and it will load the OAuth2AuthorizationRequest which is stored in the session to extract some information and repair the request with the authorization code and call to the Keycloak Authorization Server to verify the auhthorization code and exchange the Access Token and ID token if the authorization code is correct.
  • The method attemptAuthentication will be responsible for extracting OAuth2AuthorizationRequest in the OAuth2LoginAuthenticationFilter and call to the KeyCloak for getting the Access Token and ID Token.
OAuth2LoginAuthenticationFilter.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
....

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
        if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
            OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
        OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository
                .removeAuthorizationRequest(request, response);
        if (authorizationRequest == null) {
            OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
        String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
        ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
        if (clientRegistration == null) {
            OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
                    "Client Registration not found with Id: " + registrationId, null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
        // @formatter:off
        String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
                .replaceQuery(null)
                .build()
                .toUriString();
        // @formatter:on
        OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params,
                redirectUri);
        Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
        OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
                new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
        authenticationRequest.setDetails(authenticationDetails);
        OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
                .getAuthenticationManager().authenticate(authenticationRequest);
        OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter
                .convert(authenticationResult);
        Assert.notNull(oauth2Authentication, "authentication result cannot be null");
        oauth2Authentication.setDetails(authenticationDetails);
        OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
                authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
                authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());

        this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
        return oauth2Authentication;
    }

....
  • We have the code OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response); is used for getting the OAuth2AuthorizationRequest which is stored in the session. It will call the method removeAuthorizationRequest under the HttpSessionOAuth2AuthorizationRequestRepository class. The method will load OAuth2AuthorizationRequest and remove that attribute in the Session also.
HttpSessionOAuth2AuthorizationRequestRepository.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
....

    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
            HttpServletResponse response) {
        Assert.notNull(response, "response cannot be null");
        OAuth2AuthorizationRequest authorizationRequest = loadAuthorizationRequest(request);
        if (authorizationRequest != null) {
            request.getSession().removeAttribute(this.sessionAttributeName);
        }
        return authorizationRequest;
    }

....
  • Go back to the OAuth2LoginAuthenticationFilter at line OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest); we will call to the KeyCloak Provider to get the Access Token and ID Token. All these information will be stored in the OAuth2LoginAuthenticationToken and the Spring Security will extract them to continue the process in the `AbstractAuthenticationProcessingFilter.

 #zoom

  • The Spring Boot application will extract OAuth2LoginAuthenticationToken and build the OAuth2AuthenticationToken. Then this OAuth2AuthenticationToken will be saved in an attribute of the current Session by supported of the HttpSessionOAuth2AuthorizedClientRepository.
HttpSessionOAuth2AuthorizedClientRepository.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
....

    @Override
    public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal,
            HttpServletRequest request, HttpServletResponse response) {
        Assert.notNull(authorizedClient, "authorizedClient cannot be null");
        Assert.notNull(request, "request cannot be null");
        Assert.notNull(response, "response cannot be null");
        Map<String, OAuth2AuthorizedClient> authorizedClients = this.getAuthorizedClients(request);
        authorizedClients.put(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient);
        request.getSession().setAttribute(this.sessionAttributeName, authorizedClients);
    }

....
  • Then this OAuth2AuthenticationToken will be responded to the AbstractAuthenticationProcessingFilter. Then the AbstractAuthenticationProcessingFilter will set it into a new session and all the session attributes of old session will be copied to it also. This new session will be responded as a new JSESSIONID. See the method doFilter in AbstractAuthenticationProcessingFilter as below.
AbstractAuthenticationProcessingFilter.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
....

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        try {
            Authentication authenticationResult = attemptAuthentication(request, response);
            if (authenticationResult == null) {
                // return immediately as subclass has indicated that it hasn't completed
                return;
            }
            this.sessionStrategy.onAuthentication(authenticationResult, request, response);
            // Authentication success
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            successfulAuthentication(request, response, chain, authenticationResult);
        }
        catch (InternalAuthenticationServiceException failed) {
            this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
            unsuccessfulAuthentication(request, response, failed);
        }
        catch (AuthenticationException ex) {
            // Authentication failed
            unsuccessfulAuthentication(request, response, ex);
        }
    }

....
  • At line this.sessionStrategy.onAuthentication(authenticationResult, request, response); in the doFilter we will call to CompositeSessionAuthenticationStrategy.java to create a new session and copy all information of old session into it.
CompositeSessionAuthenticationStrategy.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
....

    @Override
    public void onAuthentication(Authentication authentication, HttpServletRequest request,
            HttpServletResponse response) throws SessionAuthenticationException {
        int currentPosition = 0;
        int size = this.delegateStrategies.size();
        for (SessionAuthenticationStrategy delegate : this.delegateStrategies) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace(LogMessage.format("Preparing session with %s (%d/%d)",
                        delegate.getClass().getSimpleName(), ++currentPosition, size));
            }
            delegate.onAuthentication(authentication, request, response);
        }
    }

....
  • At line delegate.onAuthentication(authentication, request, response); , the CompositeSessionAuthenticationStrategy will delegate creating and copying information from old session to new session for AbstractSessionFixationProtectionStrategy. See the code below.
AbstractSessionFixationProtectionStrategy.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
....

    @Override
    public void onAuthentication(Authentication authentication, HttpServletRequest request,
            HttpServletResponse response) {
        boolean hadSessionAlready = request.getSession(false) != null;
        if (!hadSessionAlready && !this.alwaysCreateSession) {
            // Session fixation isn't a problem if there's no session
            return;
        }
        // Create new session if necessary
        HttpSession session = request.getSession();
        if (hadSessionAlready && request.isRequestedSessionIdValid()) {
            String originalSessionId;
            String newSessionId;
            Object mutex = WebUtils.getSessionMutex(session);
            synchronized (mutex) {
                // We need to migrate to a new session
                originalSessionId = session.getId();
                session = applySessionFixation(request);
                newSessionId = session.getId();
            }
            if (originalSessionId.equals(newSessionId)) {
                this.logger.warn("Your servlet container did not change the session ID when a new session "
                        + "was created. You will not be adequately protected against session-fixation attacks");
            }
            else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug(LogMessage.format("Changed session id from %s", originalSessionId));
                }
            }
            onSessionChange(originalSessionId, session, authentication);
        }
    }

....
  • The line session = applySessionFixation(request); will call the method applySessionFixation of ChangeSessionIdAuthenticationStrategy which will return a new session which contains all information of the old one.
ChangeSessionIdAuthenticationStrategy.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
 * Copyright 2002-2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.web.authentication.session;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

/**
 * Uses {@code HttpServletRequest.changeSessionId()} to protect against session fixation
 * attacks. This is the default implementation.
 *
 * @author Rob Winch
 * @since 3.2
 */
public final class ChangeSessionIdAuthenticationStrategy extends AbstractSessionFixationProtectionStrategy {

    @Override
    HttpSession applySessionFixation(HttpServletRequest request) {
        request.changeSessionId();
        return request.getSession();
    }

}
  • Finally, let's go back to the AbstractAuthenticationProcessingFilter, the method successfulAuthentication(request, response, chain, authenticationResult); in method doFilter will be called and the Authentication will be set into the Spring Security Context.
AbstractAuthenticationProcessingFilter.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
....

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException {
        SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
        context.setAuthentication(authResult);
        this.securityContextHolderStrategy.setContext(context);
        this.securityContextRepository.saveContext(context, request, response);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
        }
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

....
  • After that, The line this.successHandler.onAuthenticationSuccess(request, response, authResult) will trigger the method onAuthenticationSuccess method in SavedRequestAwareAuthenticationSuccessHandler to continue the process.
SavedRequestAwareAuthenticationSuccessHandler.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
....

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws ServletException, IOException {
        SavedRequest savedRequest = this.requestCache.getRequest(request, response);
        if (savedRequest == null) {
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }
        String targetUrlParameter = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl()
                || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
            this.requestCache.removeRequest(request, response);
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }
        clearAuthenticationAttributes(request);
        // Use the DefaultSavedRequest URL
        String targetUrl = savedRequest.getRedirectUrl();
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }

....
  • In side the method onAuthenticationSuccess of the SavedRequestAwareAuthenticationSuccessHandler. At line SavedRequest savedRequest = this.requestCache.getRequest(request, response); , it will load the SavedRequest from the HttpSessionRequestCache and now the new session with copied information from old session from the first step will be loaded. Then the saved request URL will be responded.

  • Finally, the new JESSIONID cookie together with the redirect URL which is the first URL that the browser called /v1/secure/messages without the authorization, this redirect URL is got from the request cache from first JESSIONID will be responded as in the image above.

 #zoom

  • Then now, the browser will call the redirect URL and it will add the new JESSIONID automatically and we can get the secure message successfully.

See Also#

References#