OAuth 2.1 is an updated version of the OAuth 2.0 authorization framework, providing enhanced security features and clarifications. It addresses some of the limitations and ambiguities present in OAuth 2.0, making it more robust for modern applications. In this guide, we’ll walk through implementing OAuth 2.1 with Spring Security 6, covering client setup, authorization server configuration, and resource server integration.

What is OAuth 2.1?

OAuth 2.1 builds upon OAuth 2.0 by introducing several improvements, such as Proof Key for Code Exchange (PKCE) for public clients, safer handling of authorization codes, and more secure token exchange processes. These enhancements aim to protect against common vulnerabilities like authorization code interception and client impersonation.

What is Spring Security?

Spring Security is a comprehensive security framework for Java applications. It provides robust solutions for authentication and authorization, supporting various protocols including OAuth 2.0 and OAuth 2.1. With Spring Security 6, you can easily integrate OAuth 2.1 into your application, leveraging its powerful features and configurations.

How do I set up an OAuth 2.1 Authorization Server?

Setting up an OAuth 2.1 authorization server involves configuring Spring Security to handle client registrations, authorization grants, and token issuance. Here’s a step-by-step guide:

Step 1: Add Dependencies

First, ensure your pom.xml includes the necessary dependencies for Spring Security OAuth2:

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

Step 2: Configure the Authorization Server

Create a configuration class to set up the authorization server:

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.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore());
    }
}

Step 3: Register Clients

Define client details in a configuration file or database. For simplicity, we’ll use an in-memory client registry:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.builders.ClientDetailsServiceBuilder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;

import java.util.Arrays;

@Configuration
public class ClientConfig {

    @Bean
    public ClientDetailsService clientDetailsService() {
        return (ClientDetailsService) clients -> {
            BaseClientDetails client = new BaseClientDetails();
            client.setClientId("client");
            client.setClientSecret("{noop}secret");
            client.setAuthorizedGrantTypes(Arrays.asList("authorization_code", "refresh_token"));
            client.setScope(Arrays.asList("read", "write"));
            client.setRegisteredRedirectUri(Arrays.asList("http://localhost:8080/callback"));
            client.setAccessTokenValiditySeconds(3600);
            client.setRefreshTokenValiditySeconds(2592000);
            return client;
        };
    }
}

Step 4: Secure HTTP Endpoints

Configure HTTP security to protect your endpoints:

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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/oauth/authorize", "/login**", "/error**")
            .permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .formLogin()
            .permitAll();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
        return new InMemoryUserDetailsManager(user);
    }
}

How do I implement a Resource Server?

A resource server protects resources and verifies access tokens. Here’s how to set it up:

Step 1: Add Dependencies

Ensure your pom.xml includes the necessary dependencies for the resource server:

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

Step 2: Configure the Resource Server

Create a configuration class to set up the resource server:

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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@Configuration
@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/api/**")
            .authenticated()
            .and()
            .oauth2ResourceServer()
            .jwt();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri("http://localhost:8080/oauth2/jwks").build();
    }
}

Step 3: Secure Resources

Protect your API endpoints using Spring Security annotations:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ResourceController {

    @GetMapping("/api/resource")
    public String getResource() {
        return "This is a protected resource.";
    }
}

How do I implement PKCE for Public Clients?

Proof Key for Code Exchange (PKCE) is a security extension for OAuth 2.0 authorization code flow. It is crucial for public clients like single-page applications (SPAs) that cannot keep client secrets secure. Here’s how to enable PKCE:

Step 1: Update Client Configuration

Ensure your client is configured to use PKCE:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.builders.ClientDetailsServiceBuilder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;

import java.util.Arrays;

@Configuration
public class ClientConfig {

    @Bean
    public ClientDetailsService clientDetailsService() {
        return (ClientDetailsService) clients -> {
            BaseClientDetails client = new BaseClientDetails();
            client.setClientId("client");
            client.setClientSecret("{noop}secret");
            client.setAuthorizedGrantTypes(Arrays.asList("authorization_code", "refresh_token"));
            client.setScope(Arrays.asList("read", "write"));
            client.setRegisteredRedirectUri(Arrays.asList("http://localhost:8080/callback"));
            client.setAccessTokenValiditySeconds(3600);
            client.setRefreshTokenValiditySeconds(2592000);
            client.setAdditionalInformation(Map.of("require-proof-key", true)); // Enable PKCE
            return client;
        };
    }
}

Step 2: Update Authorization Server Configuration

Ensure your authorization server supports PKCE:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.builders.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;

@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authorizationCodeServices(authorizationCodeServices());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }
}

Security Considerations

Client Secrets

Always keep client secrets secure. Never expose them in client-side code or version control systems. Use environment variables or secure vaults to manage sensitive information.

Token Validation

Validate tokens on the resource server to ensure they are valid and have not been tampered with. This prevents unauthorized access to protected resources.

PKCE Usage

Use PKCE for public clients to mitigate authorization code interception attacks. This is especially important for SPAs and mobile apps that cannot securely store client secrets.

Troubleshooting Common Issues

Invalid Client Secret

If you encounter an “invalid_client” error, double-check that the client secret is correct and properly encoded. Ensure it matches the value stored in your client registry.

Unauthorized Access

If you receive a “401 Unauthorized” error, verify that the token is valid and has the necessary scopes. Check the token’s expiration and audience claims.

Token Endpoint Not Found

If the token endpoint is not found, ensure that your authorization server is correctly configured and running. Verify that the endpoint URL is correct and accessible.

Quick Reference

  • @EnableAuthorizationServer - Enables OAuth 2.0 authorization server support.
  • @EnableWebSecurity - Enables web security configuration.
  • TokenStore - Stores and manages OAuth 2.0 tokens.
  • JwtDecoder - Decodes and validates JWT tokens.
  • PKCE - Enhances security for public clients by preventing authorization code interception.

Comparison Table

ApproachProsConsUse When
In-Memory Token StoreSimple setupLimited scalabilityDevelopment and testing
JDBC Token StorePersistent storageRequires database setupProduction environments

Key Takeaways

  • Set up an OAuth 2.1 authorization server using Spring Security.
  • Implement a resource server to protect your API endpoints.
  • Use PKCE for public clients to enhance security.
  • Validate tokens to prevent unauthorized access.

🎯 Key Takeaways

  • Configure an OAuth 2.1 authorization server with Spring Security.
  • Implement a resource server to secure API endpoints.
  • Use PKCE for public clients to protect against authorization code interception.
  • Validate tokens to ensure secure access to resources.
💜 Pro Tip: Always keep client secrets secure and use PKCE for public clients.
⚠️ Warning: Validate tokens on the resource server to prevent unauthorized access.
🚨 Security Alert: Never expose client secrets in client-side code or version control systems.
Best Practice: Use JDBC token store for production environments to ensure persistent token storage.
💡 Key Point: PKCE is crucial for public clients to enhance security against authorization code interception.

📋 Quick Reference

  • @EnableAuthorizationServer - Enables OAuth 2.0 authorization server support.
  • @EnableWebSecurity - Enables web security configuration.
  • TokenStore - Stores and manages OAuth 2.0 tokens.
  • JwtDecoder - Decodes and validates JWT tokens.
  • PKCE - Enhances security for public clients by preventing authorization code interception.

Configure the client

Set up client details in your configuration.

Request the token

Initiate the authorization code flow to obtain a token.

Validate the response

Check the token validity before accessing resources.
graph LR A[Client] --> B[Auth Server] B --> C{Valid?} C -->|Yes| D[Access Token] C -->|No| E[Error]
Terminal
$ curl -X POST https://auth.example.com/token {"access_token": "eyJ...", "expires_in": 3600}
10x
Faster
99.9%
Uptime
< 1s
Latency

v2.0 NEW v1.5 DEPRECATED

  • Requirement 1 - completed
  • Requirement 2 - completed
  • Requirement 3 - pending
🔍 Click to see detailed explanation
Detailed content here...

Go ahead and implement OAuth 2.1 with Spring Security 6 in your projects. That’s it. Simple, secure, works.