Spring Boot 3 SAML 2.0 Integration: The “It Just Works” Guide (2026)





Spring Boot 3 SAML 2.0 Integration: The “It Just Works” Guide (2026) | www.codegigs.app





Spring Boot 3 SAML 2.0 Integration: The “It Just Works” Guide

You’re here because someone—probably an enterprise client—told you, “We require SAML SSO.” And your heart sank.

I get it. SAML has a reputation. XML signatures, metadata exchanges, keystores… it feels like 2005 all over again. But at www.codegigs.app, we’ve helped thousands of developers integrate with Okta, Azure AD, and Keycloak, and I have good news:

The nightmare is over.

Spring Security 6 (part of Spring Boot 3) completely rewrote the SAML integration. The old, painful spring-security-saml-extension is dead. The new implementation is sleek, config-driven, and doesn’t require a PhD in XML.

Here is how to set it up in 10 minutes.

Step 1: Clean Your Dependencies

First, if you have any old SAML libraries in your `pom.xml`, delete them. Burn them with fire. You don’t need `opensaml` explicitly defined anymore.

You only need the official Spring Boot starter and the SAML Service Provider module:

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- The magic module that does all the XML heavy lifting -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-saml2-service-provider</artifactId>
    </dependency>
    
    <!-- Yes, you still need the web starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Spring Boot 3 automatically pulls in OpenSAML 4 (or 5, depending on your minor version). Let it manage the versions. Trust me.

Step 2: Configuration (The Magic Part)

This is where the new Spring Security shines. Instead of writing 500 lines of XML config beans, you just point Spring to your Identity Provider’s (IdP) metadata URL.

Spring will download the keys, endpoints, and settings automatically at startup.

# application.yml
server:
  port: 8080

spring:
  security:
    saml2:
      relyingparty:
        registration:
          # "okta" is just an ID we create. Could be "azure", "keycloak", etc.
          okta:
            assertingparty:
              # REPLACE THIS with your actual IdP Metadata URL
              # Okta: https://dev-123456.okta.com/app/exk.../sso/saml/metadata
              # Keycloak: http://localhost:8080/realms/myrealm/protocol/saml/descriptor
              metadata-uri: https://dev-123456.okta.com/app/exk.../sso/saml/metadata
Critical Fix for Containers/Proxies:
If your app runs behind Nginx or inside Docker, Spring might generate redirect URLs using `http` instead of `https`, causing the IdP to reject the request. Fix it by forcing the Entity ID:

entity-id: “{baseUrl}/saml2/service-provider-metadata/{registrationId}”

Step 3: The Security Filter Chain

Now we need to tell Spring to actually use that configuration. This looks very similar to a standard OAuth2 setup.

// SecurityConfig.java
// Spring Boot 3.2+
package app.codegigs.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.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/").permitAll() // Public homepage
                .anyRequest().authenticated()     // Secure everything else
            )
            // This single line enables the entire SAML flow
            .saml2Login(withDefaults())
            
            // OPTIONAL: If you want to redirect to a specific page after login
            // .saml2Login(saml -> saml.defaultSuccessUrl("/dashboard"))
            ;
            
        return http.build();
    }
}

Line 23 registers the Saml2WebSsoAuthenticationFilter. This filter handles the redirect to Okta, processes the incoming POST response, validates the XML signature using the keys from the metadata, and establishes the user session.

Note on CSRF: Unlike stateless JWT APIs, SAML is session-based. You generally need CSRF protection enabled. Do not disable it unless you really know what you’re doing.

Step 4: Accessing User Data

The user logs in. Success! But… who are they?

In standard Spring Security, we use UserDetails. In SAML land, we use Saml2AuthenticatedPrincipal. It’s a bit different because SAML attributes are unstructured (email, department, roles, etc.) and vary wildly between providers.

// UserController.java
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserController {

    @GetMapping("/home")
    public String home(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
        // Get the "Subject" (usually username or email ID)
        String username = principal.getName();
        
        // Extract custom attributes sent by Okta/Keycloak
        // Note: Attribute names are CASE SENSITIVE and depend on your IdP!
        String email = principal.getFirstAttribute("email");
        String department = principal.getFirstAttribute("Department");
        
        model.addAttribute("username", username);
        model.addAttribute("email", email);
        model.addAttribute("allAttributes", principal.getAttributes());
        
        return "home";
    }
}

I once spent an hour trying to find “roles” in the attributes. Turns out, Azure AD sends http://schemas.microsoft.com/…/claims/groups, while Okta just sends groups. Always inspect principal.getAttributes() in your debugger first to see what keys your IdP is actually sending.

Troubleshooting: Why Is It Breaking?

SAML is fragile. If it breaks, it’s usually one of these three things:

  1. Clock Skew: If your server time is 2 minutes behind Okta’s time, the assertion is rejected as “not yet valid.” Ensure your server uses NTP.
  2. Certificate Expiry: The IdP metadata contains a public key. If that key rotates and your app doesn’t refresh the metadata, login breaks. A restart usually fixes this (forcing a fresh metadata fetch).
  3. The “Loop of Death”: App redirects to IdP -> IdP authenticates -> IdP redirects to App -> App doesn’t see session cookie -> App redirects to IdP. This is almost always a cookie issue (e.g., using http on localhost when cookies are Secure).

Need to Map SAML Groups to Spring Roles?

Mapping CN=Admin,OU=Users to ROLE_ADMIN requires a custom GrantedAuthoritiesMapper.

We cover complex role mapping and multi-tenant SAML in the full Spring Security course.

Start the Master Class

Summary

SAML doesn’t have to be scary anymore. Spring Boot 3 has turned a 3-day task into a 30-minute one.

  • Use the official spring-security-saml2-service-provider dependency.
  • Point metadata-uri to your Identity Provider.
  • Use Saml2AuthenticatedPrincipal to get user data.

Once you’ve got this working, you might want to look at something more modern. Check out our guide on Azure AD Integration if you have the choice to use OIDC instead.


Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top