SAML 2.0 SSO with Spring Boot: A Production Guide
Implement SAML 2.0 Single Sign-On in Spring Boot with Spring Security.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+
- ✓Spring Boot 3.2+
- ✓Basic knowledge of Spring Security
- ✓Access to a SAML 2.0 Identity Provider (e.g., Okta, Keycloak)
- 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.
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.
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.
Here's a minimal configuration using a metadata URL from Okta:
``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 ``
Now create a SecurityFilterChain bean:
```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.
server.forward-headers-strategy=framework.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.
Here's how to create a custom user details service:
```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); } } ```
Then register it in your security config:
``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.
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.
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:
- 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.
- 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.
- 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. - 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.
- 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.timeoutto match the IdP's session duration. - 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-ageto a lower value in production. - 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.
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); } } ```
Then configure the login filter to use this repository:
``java .saml2Login(saml2 -> saml2 .authenticationRequestRepository(tenantSaml2AuthenticationRequestRepository) ) ``
This allows you to dynamically route the user to the correct IdP.
The 2 AM Clock Skew Meltdown
assertingPartyDetails.clockSkew(Duration.ofSeconds(120)). Also added monitoring for clock drift between IdP and SP.- 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
NotOnOrAftercondition as a secondary check to avoid replay attacks.
org.springframework.security.saml2.spring.session.timeout is set appropriately.logging.level.org.springframework.security.saml2=DEBUGlogging.level.org.springframework.security.web.authentication=DEBUG| File | Command / Code | Purpose |
|---|---|---|
| pom.xml | What is SAML 2.0 and Why Should You Care? | |
| SecurityConfig.java | @Configuration | Setting Up Spring Security for SAML 2.0 |
| CustomSaml2UserService.java | @Service | Handling SAML Responses and User Details |
| logout-config.java | .saml2Logout(logout -> logout | SAML Logout |
| advanced-config.yml | spring: | What the Official Docs Won't Tell You |
| TenantSaml2AuthenticationRequestRepository.java | @Component | SAML with Multiple IdPs (Multi-Tenant SSO) |
Key takeaways
Interview Questions on This Topic
Explain the SAML 2.0 SSO flow between a Service Provider and Identity Provider.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Security. Mark it forged?
5 min read · try the examples if you haven't