Skip to main content
  1. Programming Languages/
  2. Java Enterprise Mastery: Scalable Systems & Performance Engineering/

Mastering Java Authentication: OAuth2, JWT, and SAML in Spring Boot 3

Jeff Taakey
Author
Jeff Taakey
21+ Year CTO & Multi-Cloud Architect. Bridging the gap between theoretical CS and production-grade engineering for 300+ deep-dive guides.

In the landscape of modern software development, security is not just a feature—it is the foundation. As we move through 2025, the days of simple session-based authentication for distributed systems are largely behind us. With the dominance of microservices, cloud-native architectures, and the Zero Trust security model, Java developers must master robust authentication protocols.

For mid-to-senior Java developers, the challenge often isn’t understanding what authentication is, but knowing which protocol to use and how to implement it correctly without reinventing the wheel.

In this guide, we will dissect the three titans of authentication—OAuth2, JSON Web Tokens (JWT), and SAML—and implement them using Java 21 and Spring Boot 3.4+ (utilizing Spring Security 6).

The Landscape: JWT vs. OAuth2 vs. SAML
#

Before writing code, it is crucial to understand where each technology fits. Many developers confuse these acronyms. JWT is a token format; OAuth2 is a delegation protocol; SAML is both a protocol and a format.

Here is a quick comparison to help you choose the right tool for your current architecture:

Feature JWT (JSON Web Token) OAuth2 / OIDC SAML 2.0
Primary Use Case Stateless API Authentication Delegated Authorization & Social Login Enterprise SSO (B2B, Legacy)
Format Compact JSON (Base64Url) Protocol (uses JWTs or Opaque tokens) Verbose XML
Client Type SPAs, Mobile, Microservices Web Apps, Mobile, Server-to-Server Enterprise Web Portals
Complexity Low Medium/High High
Performance High (Fast parsing) Variable (Depends on flow) Lower (XML parsing overhead)
Data Overhead Small Medium Large

Prerequisites
#

To follow this tutorial, ensure your development environment meets these standards:

  • JDK 21 (LTS)
  • Maven 3.9+ or Gradle 8.0+
  • Spring Boot 3.4.x
  • An IDE like IntelliJ IDEA or Eclipse

1. Project Setup and Dependencies
#

We will use Spring Boot 3, which integrates Spring Security 6. Spring Security 6 brought massive changes, particularly in how configuration chains are structured (moving away from WebSecurityConfigurerAdapter completely).

Create a new Maven project and add the following dependencies to your pom.xml. We are adding support for the Resource Server (JWT), the OAuth2 Client, and SAML 2 Service Provider.

<dependencies>
    <!-- Web Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Security Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- OAuth2 Resource Server (for JWT) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

    <!-- OAuth2 Client (for OIDC/Social Login) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>

    <!-- SAML2 Service Provider -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-saml2-service-provider</artifactId>
    </dependency>
    
    <!-- Lombok for boilerplates -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. Implementing Stateless Authentication with JWT
#

In 2025, JWT remains the standard for securing REST APIs. The goal is to issue a token upon login and validate it on subsequent requests without server-side session state.

The Authentication Flow
#

Below is a sequence diagram illustrating how a typical JWT flow works in a Spring Boot context.

sequenceDiagram participant User participant AuthController participant SecurityConfig participant JwtService User->>AuthController: POST /login (Credentials) AuthController->>SecurityConfig: Authenticate(Username/Password) alt Credentials Valid SecurityConfig-->>AuthController: Authentication Object AuthController->>JwtService: generateToken(Authentication) JwtService-->>AuthController: Signed JWT String AuthController-->>User: 200 OK (Bearer Token) else Invalid SecurityConfig-->>User: 401 Unauthorized end User->>AuthController: GET /api/protected (Header: Bearer Token) AuthController->>SecurityConfig: Decode & Validate JWT SecurityConfig-->>User: 200 OK (Resource Data)

Key Generation
#

For JWTs, we need RSA keys to sign (private key) and verify (public key) the tokens. In a production environment, use a Key Management Service (KMS) or secure secrets implementation. For this demo, we will generate them programmatically.

package com.javadevpro.security.config;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RsaKeyConfig {

    @Bean
    public KeyPair keyPair() {
        try {
            var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            return keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    @Bean
    public RSAPublicKey publicKey(KeyPair keyPair) {
        return (RSAPublicKey) keyPair.getPublic();
    }

    @Bean
    public RSAPrivateKey privateKey(KeyPair keyPair) {
        return (RSAPrivateKey) keyPair.getPrivate();
    }
}

Configuring the Security Filter Chain
#

Here is where the magic happens. We configure Spring Security to act as an OAuth2 Resource Server that accepts JWTs.

package com.javadevpro.security.config;

import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;

import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final RSAPublicKey publicKey;
    private final RSAPrivateKey privateKey;

    public SecurityConfig(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable()) // Disable CSRF for stateless APIs
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/token").permitAll() // Public endpoint
                        .anyRequest().authenticated() // Protect everything else
                )
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) // Enable JWT handling
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .build();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(publicKey).build();
    }

    @Bean
    JwtEncoder jwtEncoder() {
        JWK jwk = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }
}

3. Implementing OAuth2 (OIDC) Login
#

While JWT handles your internal API protection, OAuth2 (specifically OpenID Connect) is used when you want users to log in via a third party like Google, GitHub, or an enterprise Identity Provider (IdP) like Keycloak or Okta.

Spring Boot makes this incredibly simple via configuration.

Configuration (application.yml)
#

You rarely need extensive Java code for standard OAuth2 flows in Spring Boot. Add your provider details:

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: read:user
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: openid,profile,email

Updating the Security Chain
#

We need to update our SecurityFilterChain to allow for oauth2Login(). This creates a filter that intercepts the OAuth2 redirect callbacks.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/login").permitAll()
                    .anyRequest().authenticated()
            )
            // Combine JWT Resource Server with OAuth2 Login
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) 
            .oauth2Login(Customizer.withDefaults()) 
            .build();
}

When a user visits a protected endpoint via a browser, they will be redirected to the provider (GitHub/Google). Upon success, Spring creates a JSESSIONID (by default) or you can intercept the AuthenticationSuccessHandler to issue your own JWT.

4. Implementing SAML 2.0 (The Enterprise Layer)
#

SAML (Security Assertion Markup Language) is XML-based and older than OAuth2, but it remains the gold standard for large enterprise Single Sign-On (SSO) and government systems.

Implementing a SAML Identity Provider (IdP) in Java is complex and usually delegated to tools like Keycloak. However, implementing a Service Provider (SP) (your app) to accept SAML assertions is straightforward with Spring Security.

Dependencies and Configuration
#

We already added the spring-security-saml2-service-provider dependency.

You need to configure the Relying Party (your app) and the Asserting Party (the IdP). In application.yml:

spring:
  security:
    saml2:
      relyingparty:
        registration:
          my-enterprise-idp:
            signing:
              credentials:
                - private-key-location: "classpath:credentials/rp-private.key"
                  certificate-location: "classpath:credentials/rp-certificate.crt"
            assertingparty:
              metadata-uri: "https://idp.example.com/metadata.xml"

Security Chain for SAML
#

Just like OAuth2, we chain the configuration method.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .saml2Login(Customizer.withDefaults())
        .saml2Logout(Customizer.withDefaults());
    return http.build();
}

Key Note: SAML requires valid X.509 certificates. Unlike JWT secrets which can be simple strings in development, SAML will fail without proper cryptographic setup.

Best Practices and Common Pitfalls
#

1. Token Storage
#

Do not store access tokens in localStorage. This makes your application vulnerable to XSS (Cross-Site Scripting) attacks.

  • Best Practice: Store tokens in HttpOnly, Secure Cookies. This prevents JavaScript from reading the token, mitigating XSS.

2. Key Rotation
#

Hardcoding keys (as done in the demo for brevity) is a security risk.

  • Best Practice: Use environment variables or a secret vault (HashiCorp Vault, AWS Secrets Manager). Rotate keys regularly. Your JwtDecoder should be configured to check a JWK Set (JWKS) URL rather than a static public key to support rotation without downtime.

3. Audience Validation
#

When validating JWTs, always check the aud (audience) claim.

  • Best Practice: Ensure the token was issued specifically for your service, not just any service in your organization.
// Example of customizing the validator
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator("my-api-audience");
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri);
jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator));

4. CSRF Configuration
#

  • If you use JWTs in Headers (Authorization: Bearer …): You can safely disable CSRF.
  • If you use Cookies (even for JWTs): You must enable CSRF protection, as browser cookies are sent automatically.

Conclusion
#

Java authentication has evolved significantly. With Spring Boot 3 and Spring Security 6, the boilerplate required to implement industry-standard protocols like OAuth2, JWT, and SAML has been drastically reduced.

  • Use JWT for your microservices and mobile APIs.
  • Use OAuth2/OIDC for allowing users to log in with existing accounts (Google, GitHub).
  • Use SAML when integrating with legacy enterprise clients.

The code examples provided here act as a skeleton. For a production-grade system, focus heavily on the key management infrastructure and proper error handling in your security filters.

Further Reading:

Happy coding, and stay secure!

The Architect’s Pulse: Engineering Intelligence

As a CTO with 21+ years of experience, I deconstruct the complexities of high-performance backends. Join our technical circle to receive weekly strategic drills on JVM internals, Go concurrency, and cloud-native resilience. No fluff, just pure architectural execution.