Home Java SAML 2.0 SSO with Spring Boot: A Production Guide
Advanced 5 min · July 14, 2026

SAML 2.0 SSO with Spring Boot: A Production Guide

Implement SAML 2.0 Single Sign-On in Spring Boot with Spring Security.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 17+
  • Spring Boot 3.2+
  • Basic knowledge of Spring Security
  • Access to a SAML 2.0 Identity Provider (e.g., Okta, Keycloak)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use the Spring Security SAML2 extension (spring-security-saml2-service-provider) for SP-initiated SSO.
  • Configure metadata, ACS URL, and entity ID in application.yml.
  • Handle clock skew and signature validation to avoid common failures.
  • Test with a local IdP like Keycloak before production.
  • Monitor session timeouts and relay state to prevent user frustration.
✦ Definition~90s read
What is SAML 2.0 Single Sign-On with Spring Boot and Spring Security?

SAML 2.0 Single Sign-On is an XML-based standard that allows users to authenticate once at an Identity Provider and access multiple Service Providers without re-entering credentials.

Think of SAML like a VIP pass to a club.
Plain-English First

Think of SAML like a VIP pass to a club. You show your ID (login) at the door (Identity Provider), get a stamp (assertion), and then enter the club (Service Provider) without showing your ID again. The stamp is only valid for that club and that night.

If you're building a SaaS platform for enterprise customers, you've probably been asked to support SAML 2.0 SSO. It's the standard for single sign-on in corporate environments—think Okta, Azure AD, OneLogin. And if you're using Spring Boot, Spring Security has your back with the SAML 2.0 Service Provider extension.

But here's the hard truth: most teams get SAML wrong on the first try. I've seen production outages caused by clock skew, incorrect signature validation, and misconfigured ACS URLs. The official docs give you the happy path, but they don't tell you what happens when your IdP is behind a load balancer or when the user's session expires mid-flow.

In this article, I'll walk you through setting up SAML 2.0 SSO in Spring Boot 3.2 with Spring Security 6.2. I'll include a real production incident from a fintech startup that learned the hard way why you should never trust the default clock skew. By the end, you'll know not just how to configure it, but how to debug it when things go sideways.

What is SAML 2.0 and Why Should You Care?

SAML 2.0 (Security Assertion Markup Language) is the gold standard for enterprise single sign-on. It's been around since 2005, and it's not going anywhere. If your customers are large organizations, they'll likely require SAML to integrate with their identity provider (IdP) like Okta, Azure AD, or OneLogin.

SAML works by exchanging XML documents (assertions) between three parties: the user (principal), the identity provider (IdP), and the service provider (SP) — that's your Spring Boot app. The flow is simple: the user tries to access a protected resource, your app redirects them to the IdP for authentication, the IdP sends back a signed SAML assertion, and your app validates it and creates a session.

Spring Security 6.2+ includes first-class support for SAML 2.0 via the spring-security-saml2-service-provider module. This replaces the older Spring Security SAML extension, which was a third-party library. The new module is fully integrated into Spring Security's filter chain, making it much easier to configure.

Here's the key: SAML is not OAuth2. It's XML-based, not JSON. It uses digital signatures and sometimes encryption. You need to understand the concepts of assertions, conditions, and attributes. But once you set it up, it's rock solid.

pom.xmlJAVA
1
2
3
4
5
6
7
8
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
💡Version Check
📊 Production Insight
I've seen teams try to use OAuth2 for enterprise SSO because it's 'simpler'. Don't. Enterprise IdPs often require SAML for compliance reasons. Support both if you must, but SAML is non-negotiable for many organizations.
🎯 Key Takeaway
SAML 2.0 is the enterprise standard for SSO. Spring Security 6.2+ provides built-in support via the saml2-service-provider module.

Setting Up Spring Security for SAML 2.0

Let's get our hands dirty. First, add the dependencies as shown above. Then configure your application.yml with the IdP metadata. Spring Security can auto-configure from an IdP metadata URL or a local file.

``yaml spring: security: saml2: relyingparty: registration: okta: signing.credentials: - private-key-location: classpath:credentials/sp-private.key certificate-location: classpath:credentials/sp-certificate.crt assertingparty: metadata-uri: https://dev-123456.okta.com/app/abcdefg/sso/saml/metadata entity-id: https://myapp.example.com/saml2/service-provider-metadata/okta acs: location: https://myapp.example.com/login/saml2/sso/okta ``

```java @Configuration @EnableWebSecurity public class SecurityConfig {

@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) .saml2Login(saml2 -> saml2 .loginProcessingUrl("/login/saml2/sso/{registrationId}") ) .saml2Logout(logout -> logout .logoutUrl("/logout/saml2/sso") ); return http.build(); } } ```

That's it. Spring Security will automatically generate a metadata endpoint at /saml2/service-provider-metadata/{registrationId} that you can share with your IdP. The login flow is handled by the Saml2WebSecurityConfigurer.

SecurityConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .loginProcessingUrl("/login/saml2/sso/{registrationId}")
            )
            .saml2Logout(logout -> logout
                .logoutUrl("/logout/saml2/sso")
            );
        return http.build();
    }
}
📊 Production Insight
Always use HTTPS in production. SAML assertions contain sensitive data. If you're behind a load balancer, ensure your app knows the correct scheme and host via server.forward-headers-strategy=framework.
🎯 Key Takeaway
Configure SAML via application.yml and a SecurityFilterChain. Spring Security handles the SAML protocol flow automatically.

Handling SAML Responses and User Details

Once the SAML response is validated, Spring Security creates an authentication object. You can customize how user details are extracted from the assertion by providing a custom Saml2AuthenticatedPrincipal implementation.

By default, Spring Security maps the NameID as the principal name. But enterprises often send user attributes like email, roles, or department. You can access these via the getAttributes() method on the principal.

```java @Service public class CustomSaml2UserService implements Saml2UserService<Saml2UserRequest, Saml2UserDetails> {

@Override public Saml2UserDetails loadUser(Saml2UserRequest userRequest) throws AuthenticationException { Saml2AuthenticationToken token = (Saml2AuthenticationToken) userRequest.getAuthentication(); Saml2AuthenticatedPrincipal principal = token.getPrincipal(); Map<String, Object> attributes = principal.getAttributes(); String email = (String) attributes.get("email"); // Look up user in database, create if not exists return new Saml2UserDetails(principal, authorities); } } ```

``java .saml2Login(saml2 -> saml2 .userDetailsService(customSaml2UserService) ) ``

This is where you'd typically map SAML attributes to your internal user model. Be careful with attribute names — they vary between IdPs. Okta uses email, Azure AD uses http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress. Use a mapping layer.

CustomSaml2UserService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class CustomSaml2UserService implements Saml2UserService<Saml2UserRequest, Saml2UserDetails> {

    @Override
    public Saml2UserDetails loadUser(Saml2UserRequest userRequest) throws AuthenticationException {
        Saml2AuthenticationToken token = (Saml2AuthenticationToken) userRequest.getAuthentication();
        Saml2AuthenticatedPrincipal principal = token.getPrincipal();
        Map<String, Object> attributes = principal.getAttributes();
        String email = (String) attributes.get("email");
        // Look up user in database, create if not exists
        List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
        return new Saml2UserDetails(principal, authorities);
    }
}
⚠ Attribute Name Variability
📊 Production Insight
I've seen a production issue where the IdP sent attributes with different casing (email vs Email). Always normalize to lowercase or use case-insensitive comparison.
🎯 Key Takeaway
Customize user details extraction via Saml2UserService to map SAML attributes to your application's user model.

SAML Logout: SP-Initiated and IdP-Initiated

SAML supports two logout flows: SP-initiated (your app triggers logout) and IdP-initiated (the IdP triggers logout, e.g., when a user logs out of the corporate portal). Spring Security supports both.

For SP-initiated logout, you need a logout endpoint and a way to send a logout request to the IdP. Configure it like this:

``java .saml2Logout(logout -> logout .logoutUrl("/logout/saml2/sso") .logoutSuccessUrl("/") ) ``

This will send a <LogoutRequest> to the IdP's single logout service. The IdP then responds with a <LogoutResponse>. Spring Security handles the response and ends the local session.

For IdP-initiated logout, the IdP sends a <LogoutRequest> to your app's logout endpoint. Spring Security's Saml2LogoutFilter handles this automatically if you configure the logout processing URL.

Here's a common gotcha: the IdP may not support single logout (SLO). In that case, you can disable SAML logout and just use local session invalidation. But enterprises often require SLO for compliance.

To test, you can use a tool like the SAML Chrome extension to inspect the requests and responses.

logout-config.javaJAVA
1
2
3
4
5
.saml2Logout(logout -> logout
    .logoutUrl("/logout/saml2/sso")
    .logoutSuccessUrl("/")
    .logoutRequestUrl("/logout/saml2/sso/{registrationId}")
)
📊 Production Insight
Some IdPs require the logout request to be signed. Spring Security signs it by default if you have signing credentials configured. If you get signature errors, check that the IdP's certificate is imported correctly.
🎯 Key Takeaway
Configure both SP-initiated and IdP-initiated logout. Ensure the IdP supports SLO, or fall back to local session invalidation.

What the Official Docs Won't Tell You

The Spring Security reference guide is good, but it glosses over the messy reality. Here are the gotchas I've encountered:

  1. Clock Skew is a Silent Killer: As mentioned in the production incident, default clock skew is 60 seconds. In my experience, you need 120-180 seconds to account for network latency and IdP clock drift. Always set it explicitly.
  2. ACS URL Must Match Exactly: The ACS (Assertion Consumer Service) URL in your app must match what the IdP expects. This includes the protocol (https), domain, and trailing slash. A mismatch results in a 403 or silent redirect loop.
  3. Multiple IdPs Require Registration IDs: If you support multiple IdPs (e.g., different customers), each needs a unique registration ID. The ACS URL pattern is /login/saml2/sso/{registrationId}. Make sure your IdP is configured to POST to the correct URL.
  4. Relay State Can Be a Security Issue: The relay state parameter is used to preserve the original request URL. But if an attacker manipulates it, they can redirect the user after login. Spring Security validates relay state by default, but if you disable it, you're vulnerable.
  5. Session Timeouts Mismatch: Your app's session timeout may be longer than the SAML session timeout. When the SAML assertion expires, the user should be forced to re-authenticate. Configure spring.session.timeout to match the IdP's session duration.
  6. Metadata Endpoint Caching: Spring Security caches the IdP metadata by default. If the IdP updates its certificates, your app won't pick up the change until the cache expires (default 1 hour). Set assertingparty.metadata-cache-max-age to a lower value in production.
  7. Signature Algorithm Mismatch: Older IdPs may use SHA-1 signatures, which are deprecated. Spring Security uses SHA-256 by default. If your IdP sends SHA-1, you'll get a validation error. You can configure the algorithm in assertingparty.signature-algorithms.
advanced-config.ymlJAVA
1
2
3
4
5
6
7
8
9
10
11
spring:
  security:
    saml2:
      relyingparty:
        registration:
          okta:
            assertingparty:
              metadata-uri: https://dev-123456.okta.com/app/abcdefg/sso/saml/metadata
              clock-skew: 120s
              metadata-cache-max-age: 10m
              signature-algorithms: ["http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"]
🔥Metadata Cache
📊 Production Insight
Always test with a real IdP, not just a mock. Use a free Okta developer account or a local Keycloak instance. Mocking SAML responses often misses real-world edge cases.
🎯 Key Takeaway
Production SAML has many pitfalls: clock skew, ACS URL exactness, relay state security, and signature algorithm mismatches. Configure them explicitly.

SAML with Multiple IdPs (Multi-Tenant SSO)

If you're building a SaaS platform, you'll likely need to support multiple IdPs — one per customer. Spring Security supports this out of the box via multiple relying party registrations.

Simply add multiple entries under spring.security.saml2.relyingparty.registration. Each registration needs a unique ID (e.g., customer1, customer2). The ACS URL will include the registration ID: /login/saml2/sso/{registrationId}.

You'll need a way to determine which IdP to use for a given user. Common approaches: - Subdomain: customer1.myapp.com -> IdP for customer1 - Email domain: user@customer1.com -> IdP for customer1 - Explicit login page: User selects their company from a dropdown

Here's an example of a custom Saml2AuthenticationRequestRepository that selects the registration based on a request parameter:

```java @Component public class TenantSaml2AuthenticationRequestRepository implements Saml2AuthenticationRequestRepository<Saml2AuthenticationRequest> {

@Override public Saml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) { String tenantId = request.getParameter("tenant"); if (tenantId == null) { throw new AuthenticationServiceException("Missing tenant parameter"); } return new Saml2AuthenticationRequest(tenantId, null); } } ```

``java .saml2Login(saml2 -> saml2 .authenticationRequestRepository(tenantSaml2AuthenticationRequestRepository) ) ``

This allows you to dynamically route the user to the correct IdP.

TenantSaml2AuthenticationRequestRepository.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class TenantSaml2AuthenticationRequestRepository
        implements Saml2AuthenticationRequestRepository<Saml2AuthenticationRequest> {

    @Override
    public Saml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
        String tenantId = request.getParameter("tenant");
        if (tenantId == null) {
            throw new AuthenticationServiceException("Missing tenant parameter");
        }
        return new Saml2AuthenticationRequest(tenantId, null);
    }

    @Override
    public void saveAuthenticationRequest(Saml2AuthenticationRequest request, HttpServletRequest req, HttpServletResponse resp) {}

    @Override
    public Saml2AuthenticationRequest removeAuthenticationRequest(HttpServletRequest request, HttpServletResponse response) {
        return loadAuthenticationRequest(request);
    }
}
📊 Production Insight
Be careful with registration ID collisions. Use a unique, URL-safe identifier for each customer. I've seen issues with special characters in registration IDs causing routing failures.
🎯 Key Takeaway
Support multiple IdPs by registering multiple relying parties and using a custom request repository to select the correct one.
● Production incidentPOST-MORTEMseverity: high

The 2 AM Clock Skew Meltdown

Symptom
Users in Europe got 'Invalid assertion: NotBefore condition not met' errors during SAML login, while US users worked fine.
Assumption
The developer assumed the IdP and SP clocks were in sync since both used NTP.
Root cause
The IdP server had a 30-second clock drift (fast), and Spring Security's default clock skew tolerance was only 60 seconds. But the assertion's NotBefore time was set to the IdP's time, which was 30 seconds in the future from the SP's perspective. Combined with network latency, some assertions arrived before their NotBefore time.
Fix
Increased clock skew tolerance to 120 seconds in the RelyingPartyRegistration configuration: assertingPartyDetails.clockSkew(Duration.ofSeconds(120)). Also added monitoring for clock drift between IdP and SP.
Key lesson
  • Always set clock skew tolerance to at least 120 seconds in production.
  • Monitor NTP synchronization on both IdP and SP servers.
  • Add logging for SAML assertion validation failures to capture the actual time difference.
  • Test with a real IdP in a different timezone or with artificial clock skew.
  • Consider using the NotOnOrAfter condition as a secondary check to avoid replay attacks.
Production debug guideSymptom to Action4 entries
Symptom · 01
User redirected to IdP but never returns to app (infinite loop or blank page).
Fix
Check ACS URL configuration. Ensure it matches exactly (including trailing slash) what the IdP expects. Enable DEBUG logging for org.springframework.security.saml2.
Symptom · 02
HTTP 403 after SAML response.
Fix
Check signature validation. Ensure the IdP's X.509 certificate is correctly imported. Verify the algorithm (RSA-SHA256 vs SHA1).
Symptom · 03
'Invalid assertion' with NotBefore condition.
Fix
Check clock skew. Compare SP and IdP system times. Increase clock skew tolerance or fix NTP.
Symptom · 04
User logged in but session expires immediately.
Fix
Check session timeout configuration. SAML session may be shorter than web session. Ensure spring.session.timeout is set appropriately.
★ Quick Debug Cheat SheetCommon SAML SSO issues and immediate actions.
SAML response not accepted
Immediate action
Enable DEBUG logging for SAML2 filter chain
Commands
logging.level.org.springframework.security.saml2=DEBUG
logging.level.org.springframework.security.web.authentication=DEBUG
Fix now
Check signature, clock skew, and ACS URL mismatch
User not redirected to IdP+
Immediate action
Check if request is authenticated and requires SAML
Commands
curl -v http://localhost:8080/protected
Check SecurityFilterChain configuration
Fix now
Ensure SAML2 login filter is before any authorization filter
Blank page after IdP login+
Immediate action
Check browser console for redirect errors
Commands
Look for CORS or relay state issues
Verify IdP sends correct RelayState
Fix now
Set relay state validation to 'always' or disable if not needed
FeatureSAML 2.0OAuth2
ProtocolXML-basedJSON-based
Use CaseEnterprise SSOAPI delegation
Token FormatAssertionJWT
State ManagementServer-side sessionsStateless tokens
Spring Supportspring-security-saml2-service-providerspring-security-oauth2-client
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
pom.xmlWhat is SAML 2.0 and Why Should You Care?
SecurityConfig.java@ConfigurationSetting Up Spring Security for SAML 2.0
CustomSaml2UserService.java@ServiceHandling SAML Responses and User Details
logout-config.java.saml2Logout(logout -> logoutSAML Logout
advanced-config.ymlspring:What the Official Docs Won't Tell You
TenantSaml2AuthenticationRequestRepository.java@ComponentSAML with Multiple IdPs (Multi-Tenant SSO)

Key takeaways

1
SAML 2.0 SSO is essential for enterprise customers. Spring Security 6.2+ provides robust support via the saml2-service-provider module.
2
Configure clock skew, signature algorithms, and metadata caching explicitly to avoid production issues.
3
Support multiple IdPs with separate relying party registrations and a dynamic request repository.
4
Test with a real IdP like Keycloak or Okta, not just mocks. Enable DEBUG logging for troubleshooting.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the SAML 2.0 SSO flow between a Service Provider and Identity Pr...
Q02SENIOR
How would you troubleshoot a 'NotBefore condition not met' error in SAML...
Q03JUNIOR
What is the purpose of the RelayState parameter in SAML?
Q01 of 03SENIOR

Explain the SAML 2.0 SSO flow between a Service Provider and Identity Provider.

ANSWER
The user requests a protected resource on the SP. The SP redirects the user to the IdP with a SAML authentication request. The user authenticates at the IdP. The IdP sends a SAML response (assertion) to the SP's ACS endpoint. The SP validates the assertion (signature, conditions, audience) and creates a session.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between SAML and OAuth2?
02
How do I test SAML locally?
03
Can I use SAML without Spring Security?
04
What happens if the IdP goes down?
05
How do I handle SAML attribute mapping?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Security. Mark it forged?

5 min read · try the examples if you haven't

Previous
Handling Spring Security Exceptions with @ExceptionHandler and AccessDeniedHandler
28 / 31 · Spring Security
Next
Disabling Spring Security for a Specific Profile in Spring Boot