Enterprise Java

Secure SPA Authentication with PKCE and Spring Authorization Server

This article will explore implementing authentication in a Single Page Application (SPA) using the Proof Key for Code Exchange (PKCE) extension in the Spring Authorization Server. PKCE is a security extension that provides an additional layer of security for OAuth 2.0 authorization flows.

1. What is PKCE?

PKCE is a security enhancement that reduces the risk of authorization code interception attacks in OAuth 2.0. It adds an extra layer of security by requiring the client to generate a code challenge and code verifier pair. The code challenge is sent with the authorization request, and the code verifier is used during the token exchange. This ensures that only the client that initiated the authorization request can complete the authorization process.

1.1 The Flow of Authentication with PKCE

Here is a graphical representation of the authentication flow with PKCE:

Figure 1: authentication process using PKCE in Spring for Single Page Applications

1.2 Explanation of the Flow

  1. Generate Code Verifier and Code Challenge: The client generates a cryptographically random code verifier and computes a code challenge derived from the code verifier.
  2. Authorization Request: The client initiates the authorization request to the authorization server, including the code challenge and specifying the code challenge method (S256).
  3. User Authentication and Authorization: The user authenticates and authorizes the client application.
  4. Authorization Code: Upon successful authorization, the authorization server redirects back to the client with an authorization code.
  5. Token Request: The client makes a token request to the authorization server, including the authorization code, the code verifier, and other necessary parameters.
  6. Token Response: The authorization server validates the code verifier against the code challenge and issues an access token if valid.
  7. Access Protected Resource: The client uses the access token to request protected resources from the resource server.
  8. Protected Resource: The resource server responds with the requested protected resource.

2. Setting Up the Spring Authorization Server

The Spring Authorization Server supports PKCE. To add PKCE support to the application, include the spring-boot-starter-oauth2-authorization-server dependency in the pom.xml file:

   <dependency>
         <groupId>org.springframework.boot</groupId>
	 <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
   </dependency>

2.1 YAML Template for Spring Authorization Server Configuration

This YAML configuration file sets up the Spring Authorization Server with settings necessary for handling OAuth2 and OpenID Connect (OIDC) flows.

## YAML Template.
---
server:
  port: 8000
  
spring:
  security:
    oauth2:
      authorizationserver:
        client:
          public-client:
            registration:
              client-id: "spa-client"
              client-authentication-methods:
                - "none"
              authorization-grant-types:
                - "authorization_code"
              redirect-uris:
                - "http://127.0.0.1:5000/callback.html"
                - "http://127.0.0.1:5000"
                - "http://127.0.0.1:5000/silent-renew.html"
              scopes:
                - "openid"
                - "profile"
                - "email"
            require-authorization-consent: true
            require-proof-key: true

This file configures the server to run on port 8000.

  • client-id: Specifies the unique identifier for the client application. In this case, it’s set to “spa-client”.
  • client-authentication-methods: Indicates the authentication method used by the client. Here, “none” means that no client authentication is required, suitable for public clients like SPAs.
  • authorization-grant-types: Lists the types of authorization grants that the client can use. “authorization_code” is included, which is appropriate for SPAs to securely obtain tokens.
  • redirect-uris: Defines the URIs where the authorization server will redirect the client after successful authentication. The URIs include:
    • http://127.0.0.1:5000/callback.html: The primary redirect URI after login.
    • http://127.0.0.1:5000: Another acceptable redirect URI.
    • http://127.0.0.1:5000/silent-renew.html: Used for silent token renewal without user interaction.
  • scopes: Specifies the scopes that the client can request. “openid”, “profile”, and “email” are included to access basic user information.
  • require-authorization-consent: Set to true to require explicit user consent for the requested scopes.
  • require-proof-key: Set to true to enforce Proof Key for Code Exchange (PKCE), enhancing security by mitigating authorization code interception attacks.

2.2 Configuring Spring Authorization Server with PKCE

This code block configures the security settings for the Spring Authorization Server, including support for OAuth2, OpenID Connect (OIDC), and PKCE. The configuration includes setting up security filter chains, user details, password encoding, and CORS settings.

@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {

    @Bean
    @Order(1)
    SecurityFilterChain configure(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());
        http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML)))
                .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
        return http.cors(Customizer.withDefaults())
                .build();
    }

    @Bean
    @Order(2)
    SecurityFilterChain defaultConfigure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize.anyRequest()
                .authenticated())
                .formLogin(Customizer.withDefaults());
        return http.cors(Customizer.withDefaults())
                .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User
                .withUsername("thomas")
                .password(encoder().encode("paine"))
                .roles("ADMIN").build());
        manager.createUser(User
                .withUsername("bill")
                .password(encoder().encode("withers"))
                .roles("USER").build());
        return manager;
    }

    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source
                = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOriginPattern("*");
        config.addAllowedOrigin("http://127.0.0.1:5000");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

}

  • The configure method configures the security filter chain for the OAuth2 Authorization Server. The OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) method applies default security settings for the authorization server. It also enables OpenID Connect (OIDC) by calling http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults()). This setup ensures the server is configured to handle PKCE for secure authentication.
  • Exception handling is configured to use a custom login entry point for HTML requests, while the OAuth2 resource server is set up to handle JWT tokens. The method returns a configured security filter chain with CORS support enabled.
  • The defaultConfigure method sets up a second security filter chain that ensures all requests are authenticated. It also enables form-based login with default settings. This method provides a fallback security configuration for any remaining requests not handled by the first filter chain. It also returns a security filter chain with CORS support.
  • The userDetailsService bean creates an in-memory user details manager with two users: “thomas” (with the role ADMIN) and “bill” (with the role USER). Passwords for these users are encoded using the BCrypt encoder provided by the encoder bean.
  • The corsFilter bean sets up a CORS filter that allows all origins, headers, and methods. This configuration is registered for all paths (/**) in the application, enabling cross-origin requests to be handled correctly.

3. Implementing PKCE in the SPA

On the client side, we will utilize the oidc-client-ts library to support OIDC and OAuth2.

3.1 Frontend: Vue.js Application Setup

The AuthService class shown below provides authentication functionalities using the oidc-client library. This class handles user authentication and token management for our Vue application.

import { UserManager, WebStorageStateStore, User } from 'oidc-client';

export default class AuthService {
    private userManager: UserManager;

    constructor() {
        const SAS_DOMAIN: string = 'http://127.0.0.1:8000';

        const settings: any = {
            userStore: new WebStorageStateStore({ store: window.localStorage }),
            authority: SAS_DOMAIN,
            client_id: 'spa-client',
            redirect_uri: 'http://127.0.0.1:5000/callback.html',
            automaticSilentRenew: true,
            silent_redirect_uri: 'http://127.0.0.1:5000/silent-renew.html',
            response_type: 'code',
            scope: 'openid profile email',
            post_logout_redirect_uri: 'http://127.0.0.1:5000/',
        };

        this.userManager = new UserManager(settings);
    }

    public getUser(): Promise<User | null> {
        return this.userManager.getUser();
    }

    public login(): Promise<void> {
        return this.userManager.signinRedirect();
    }

    public logout(): Promise<void> {
        return this.userManager.signoutRedirect();
    }

    public getAccessToken(): Promise<string> {
        return this.userManager.getUser().then((data: any) => {
            return data.access_token;
        });
    }
}

The above code snippet:

  • Imports the necessary components from oidc-client: UserManager, WebStorageStateStore, and User.
  • Constructor:
    • Initializes the userManager with settings for interacting with the OIDC provider.
    • SAS_DOMAIN: The domain of the Spring Authorization Server.
    • userStore: Uses localStorage to store user information.
    • authority: The URL of the OIDC provider.
    • client_id: The client identifier for the SPA.
    • redirect_uri: The URL to redirect to after login.
    • automaticSilentRenew: Enables silent token renewal.
    • silent_redirect_uri: The URL for silent token renewal.
    • response_type: The response type for the authentication request (e.g., ‘code’).
    • scope: The scopes requested (e.g., ‘openid’, ‘profile’, ’email’).
    • post_logout_redirect_uri: The URL to redirect to after logout.
  • Methods:
    • getUser(): Retrieves the current user information.
    • login(): Initiates the login process using a redirect.
    • logout(): Initiates the logout process using a redirect.
    • getAccessToken(): Retrieves the access token for the current user.

4. Running the Application

4.1 Running the Spring Authorization Server

The Spring Boot application is configured with the provided application.yml settings. The Spring Authorization Server should run on http://127.0.0.1:8000

4.2 Running the Vue.js Application

Ensure your Vue.js application is configured with the necessary oidc-client-ts settings. Run the Vue.js application using the following command:

npm run serve

The Vue application should start and be accessible on http://localhost:5000.

Logging In

Open a web browser and go to http://localhost:5000. You should see the home page of the Vue.js application with a “Login” button.

Initiate Login

Click the “Login” button. This action triggers the login method in the Vue.js application, which redirects the user to the authorization server for authentication.

Authorization Server Login Page

You will be redirected to the Spring Authorization Server login page. Enter your credentials and click “Sign In.”. The SPA sends a request to the Spring Authorization Server with the code_challenge and code_challenge_method, as shown in the screenshot below.

Authorization Consent

If configured, the authorization server will prompt the user for consent to grant access to the requested scopes. Click “Submit Consent” to proceed.

Redirect Back to SPA

After successful authentication and authorization, you will be redirected back to the Vue.js application at http://localhost:5000/callback.html. The Callback component will handle the response, extract the user information, and store it.

Once authenticated, you should see a welcome message on the home page, displaying the user’s name or other profile information.

5. Conclusion

In this article, we explored how to secure authentication for Single Page Applications (SPAs) using PKCE with Spring Authorization Server. We covered the importance of PKCE in enhancing security by mitigating authorization code interception attacks. By following the outlined steps, you can implement a secure authentication mechanism that ensures the integrity and confidentiality of user data.

6. Download the Source Code

This article covered Spring authentication for a single page application using PKCE.

Download
You can download the full source code of this example here: spring authentication single page application pkce

Omozegie Aziegbe

Omos holds a Master degree in Information Engineering with Network Management from the Robert Gordon University, Aberdeen. Omos is currently a freelance web/application developer who is currently focused on developing Java enterprise applications with the Jakarta EE framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button