Integrating a Spring Boot App with SAML SSO (Hands-On Tutorial)
Introduction
A practical, end‑to‑end guide to enabling SAML‑based Single Sign‑On (SSO) in a Spring Boot application using Spring Security’s SAML2 Service Provider support (Spring Boot 3.x / Spring Security 6.x). Includes working code, config, metadata, and troubleshooting tips.
What you’ll build
- A Spring Boot web app (Service Provider, SP) that authenticates users via an external Identity Provider (IdP) such as Okta, Azure AD, Keycloak, or OneLogin.
- SAML 2.0 login, role (authority) mapping, and logout.
- Local dev + production‑ready configurations.
Prerequisites
- Java 17+
- Maven or Gradle
- Spring Boot 3.x
- An IdP tenant (Okta Dev, Azure AD, Keycloak, OneLogin, or https://samltest.id for quick tests)
Note: Spring Security’s legacyspring-security-samlproject is deprecated. Use the modernspring-security-saml2-service-providermodule (built into Spring Security 5.2+).
Architecture quick view
- User requests a protected page in the SP.
- SP issues an AuthnRequest → IdP via browser redirect.
- IdP authenticates the user; posts a signed SAML Response with Assertion to the SP’s ACS endpoint.
- SP verifies signature, audience, issuer, and conditions; creates a Spring Security
Authenticationwith user attributes & authorities.
Key endpoints (Spring defaults):
- Login redirect:
/saml2/authenticate/{registrationId} - ACS (Assertion Consumer Service):
/login/saml2/sso/{registrationId} - SP metadata:
/saml2/service-provider-metadata/{registrationId}
1) Start a Spring Boot project
Maven pom.xml:
<project>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.3.0</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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>
<!-- Optional for key generation from PKCS12 keystores -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Gradle (Kotlin DSL) build.gradle.kts:
plugins {
id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
kotlin("jvm") version "1.9.24" apply false // remove if not using Kotlin
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.security:spring-security-saml2-service-provider")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } }
2) Generate SP signing keys
SAML usually requires the SP to sign AuthnRequests and publish its certificate in metadata.
Create a PKCS#12 keystore and certificate:
keytool -genkeypair \
-alias sp-signing \
-keyalg RSA -keysize 2048 -sigalg SHA256withRSA \
-keystore sp-keystore.p12 -storetype PKCS12 \
-storepass changeit -keypass changeit \
-dname "CN=Demo SP, OU=Dev, O=Example, L=City, ST=State, C=US"
Place sp-keystore.p12 under src/main/resources/.
(If your IdP requires separate encryption keys, generate a second pair.)
3) Application properties (YAML)
Create src/main/resources/application.yml with one SAML registration (e.g., okta). Replace placeholders with your IdP values.
server:
port: 8080
spring:
security:
saml2:
relyingparty:
registration:
okta: # ← registrationId
signing:
credentials:
- private-key-location: classpath:sp-keystore.p12
certificate-location: classpath:sp-keystore.p12
private-key-password: changeit
# Spring can read PKCS12; for JKS use key-alias/key-store
assertingparty:
metadata-uri: https://dev-XXXXX.okta.com/app/XXXXXX/sso/saml/metadata
# OR define manually instead of metadata-uri:
# entity-id: "http://www.okta.com/XXXXXX"
# singlesignon.url: "https://dev-XXXXX.okta.com/app/XXXXXX/sso/saml"
# verification.credentials:
# - certificate-location: classpath:okta.cer
thymeleaf:
cache: false
# Optional: show logs during SAML debug
logging:
level:
org.springframework.security.saml2: DEBUG
org.springframework.security.web: DEBUG
Tip: You can have multiple registrations (okta,azure,keycloak). Each appears under/saml2/service-provider-metadata/{registrationId}and/saml2/authenticate/{registrationId}.
4) Security configuration (Java)
SecurityConfig.java:
package com.example.saml;
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.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.saml2Login(Customizer.withDefaults())
.logout(logout -> logout
.logoutSuccessUrl("/")
);
return http.build();
}
}
Attribute → authorities mapping (optional)
If your IdP sends a groups/roles attribute, map them to Spring authorities:
@Bean
AuthenticationConverter samlAuthoritiesConverter() {
return (responseToken) -> {
var auth = responseToken.getAuthentication();
var attributes = responseToken.getToken().getSaml2Response(); // raw XML if needed
// Prefer the higher-level API:
// responseToken.getToken().getSignedAssertions(); // parse with OpenSAML if necessary
// For simplicity, use default authorities and return
return auth; // or enrich with mapped GrantedAuthorities
};
}
In Spring Security 6, you typically customize authorities viaSaml2AuthenticationTokenConverterandGrantedAuthoritiesMapper, or a customAuthenticationSuccessHandler.
5) Minimal MVC to test
HomeController.java:
package com.example.saml;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String index() { return "index"; }
@GetMapping("/profile")
public String profile(Authentication authentication, Model model) {
model.addAttribute("auth", authentication);
return "profile";
}
}
src/main/resources/templates/index.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Home</title></head>
<body>
<h1>Welcome</h1>
<p><a href="/saml2/authenticate/okta">Login with SAML (Okta)</a></p>
<p><a href="/profile">Profile (protected)</a></p>
</body>
</html>
src/main/resources/templates/profile.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Profile</title></head>
<body>
<h1>Profile</h1>
<p th:text="${auth}"></p>
<p><a href="/logout">Logout</a></p>
</body>
</html>
6) Obtain & publish metadata
6.1 Import IdP metadata into the SP
Set assertingparty.metadata-uri to your IdP’s metadata endpoint (Okta, Azure, Keycloak). Spring will pull signing certificates and endpoints automatically at startup.
6.2 Give SP metadata to the IdP
Start the app, then open:
http://localhost:8080/saml2/service-provider-metadata/okta
This XML contains the SP entityID, ACS URL(s), and certificate(s). Upload it to your IdP when configuring the app integration.
Some IdPs require a static entityID. You can set it via spring.security.saml2.relyingparty.registration.okta.entity-id.7) IdP‑specific notes
Okta
- Create a SAML 2.0 app integration.
- ACS URL:
http://localhost:8080/login/saml2/sso/okta - SP Entity ID:
http://localhost:8080/saml2/service-provider-metadata/okta(or custom) - NameID format:
emailAddressorunspecified. - Attribute statements:
firstName,lastName,groups.
Azure AD (Enterprise App)
- Set Identifier (Entity ID) and Reply URL (ACS) as above.
- Download Federation Metadata XML and use its URL.
- Azure often sends NameID = user.userprincipalname.
Keycloak
- Create a client with Client Protocol = saml.
- Set Valid Redirect URIs to
/login/saml2/sso/*. - Export SAML 2.0 Identity Provider Metadata.
8) Custom attribute extraction & role mapping
Implement a GrantedAuthoritiesMapper to map SAML attributes to roles:
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
@Bean
GrantedAuthoritiesMapper authoritiesMapper() {
return (authorities) -> {
var mapped = new java.util.HashSet<GrantedAuthority>(authorities);
var samlPrincipal = authorities.stream()
.filter(a -> a instanceof org.springframework.security.authentication.AbstractAuthenticationToken)
.findFirst();
// Alternatively access in a controller via Authentication.getPrincipal()
return mapped;
};
}
A simpler approach is to access attributes in controllers:
@GetMapping("/me")
@ResponseBody
public Map<String,Object> me(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) {
return Map.of(
"name", principal.getName(),
"attributes", principal.getAttributes()
);
}
9) Logout & (optional) SLO
- App logout: Spring clears session at
/logoutand can redirect to home. - IdP‑initiated or SP‑initiated Single Logout (SLO): Limited out‑of‑the‑box; many teams use a post‑logout URL at the IdP. Some IdPs support redirecting back to the app.
10) Running end‑to‑end
- Start the app:
mvn spring-boot:runor./mvnw spring-boot:run. - Open
http://localhost:8080/and click Login with SAML (Okta). - Authenticate at the IdP; you should land on
/profilewith an authenticated principal. - Visit
/me(if you added it) to inspect SAML attributes.
11) Common pitfalls & fixes
- Clock skew: Assertions not yet valid → ensure servers are NTP‑synced; configure
clockSkewat the IdP if available. - Audience/Entity mismatch: Make sure IdP’s Audience URI matches your SP entityId.
- Signature failures: Rotate IdP signing certs—refresh metadata or update
verification.credentials. - Multiple ACS endpoints: Use the exact
/login/saml2/sso/{registrationId}in IdP settings. - HTTPS vs HTTP: Behind reverse proxies, set
X-Forwarded-*headers orserver.forward-headers-strategy=framework. - NameID format: If IdP sends a different format, adjust IdP config or map principal via a custom converter.
12) Production hardening checklist
- Enforce HTTPS (HSTS) and secure cookies.
- Rotate SP signing keys; store secrets in a vault.
- Restrict allowed audiences/issuers; validate InResponseTo.
- Limit assertion lifetimes; reduce clock skew.
- Map & scope attributes minimally; avoid over‑privilege.
- Configure session timeout & CSRF (enabled by default with Spring Security).
13) Bonus: Multiple IdPs (multi‑tenant)
Add additional registrations under spring.security.saml2.relyingparty.registration. Expose distinct login links:
<a href="/saml2/authenticate/okta">Login with Okta</a>
<a href="/saml2/authenticate/azure">Login with Azure AD</a>
<a href="/saml2/authenticate/keycloak">Login with Keycloak</a>
14) Directory structure (reference)
src/
main/
java/com/example/saml/
SecurityConfig.java
HomeController.java
resources/
application.yml
sp-keystore.p12
templates/
index.html
profile.html
15) Next steps
- Add a custom
AuthenticationSuccessHandlerto redirect based on roles. - Persist sessions in Redis for horizontal scaling.
- Implement attribute transformation (e.g., map
groups→ROLE_*). - Add tests with MockMvc and sample SAML responses.
Appendix A — Manual asserting party (no metadata URI)
spring:
security:
saml2:
relyingparty:
registration:
keycloak:
entity-id: "https://your-app.example.com/saml/sp"
signing:
credentials:
- private-key-location: classpath:sp-keystore.p12
certificate-location: classpath:sp-keystore.p12
private-key-password: changeit
assertingparty:
entity-id: "http://keycloak/realms/demo"
singlesignon:
url: "https://keycloak.example.com/realms/demo/protocol/saml"
binding: redirect
verification:
credentials:
- certificate-location: classpath:keycloak.cer
Appendix B — Using SAMLtest.id
- Metadata URI:
https://samltest.id/saml/sp - ACS:
http://localhost:8080/login/saml2/sso/samltest - Create a
samltestregistration with the provided IdP metadata and test credentials.
You’re done! You now have a working Spring Boot SAML SSO integration you can adapt for your IdP. Copy‑paste the snippets above, swap the metadata and certificates, and ship it.