SAML 2.0 sigue siendo el estándar de SSO empresarial dominante en grandes corporaciones, sobre todo en sectores regulados (banca, sanidad, administración pública). Aunque OAuth2 + OpenID Connect sea más moderno, los IdPs corporativos como Entra ID (antes Azure AD), Okta, PingFederate o ADFS suelen exigir SAML para integraciones con aplicaciones internas.
Spring Security 7.x incluye soporte nativo de SAML 2.0 sin librerías externas como Spring SAML o OpenSAML directo. Esta lección cubre el flujo, la configuración mínima y los puntos de integración con un IdP real.
El flujo SAML 2.0 Web SSO
El flujo más común es el SP-initiated con HTTP-Redirect para la petición y HTTP-POST para la respuesta.
sequenceDiagram
participant U as Usuario
participant SP as App Spring (SP)
participant IdP as Entra ID / Okta
U->>SP: GET /privado
SP->>U: 302 Redirect a IdP con AuthnRequest
U->>IdP: GET /sso?SAMLRequest=...
IdP->>U: Form de login
U->>IdP: POST credenciales
IdP->>U: HTML auto-submit con SAMLResponse
U->>SP: POST /login/saml2/sso/{registrationId} con SAMLResponse
SP->>SP: Verificar firma y crear SecurityContext
SP->>U: 302 a /privado
U->>SP: GET /privado autenticado
Los conceptos clave son:
- Service Provider (SP): la aplicación Spring que delega la autenticación.
- Identity Provider (IdP): Entra ID, Okta, etc. Mantiene las credenciales y emite assertions.
- SAMLRequest: petición XML firmada que el SP envía al IdP solicitando autenticación.
- SAMLResponse: respuesta XML firmada del IdP con las assertions sobre el usuario.
- Assertion: bloque XML con
Subject,Conditions,AuthnStatementyAttributeStatement.
Dependencias y configuración mínima
<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>
El SP necesita un certificado y clave privada para firmar AuthnRequest y descifrar EncryptedAssertion. Generamos un par autofirmado con keytool.
keytool -genkeypair -alias sp -keyalg RSA -keysize 2048 \
-validity 365 -keystore sp.p12 -storetype PKCS12 \
-storepass secret -dname "CN=spring-sp"
keytool -exportcert -alias sp -keystore sp.p12 \
-storepass secret -file sp-cert.crt -rfc
Y declaramos un relying party registration en application.yaml apuntando a la metadata del IdP, que automáticamente descubre URL de SSO, certificado de firma y entidad ID.
spring:
security:
saml2:
relyingparty:
registration:
entra:
entity-id: "https://app.demo.local/saml2/sp"
assertingparty:
metadata-uri: "https://login.microsoftonline.com/{tenant-id}/federationmetadata/2007-06/federationmetadata.xml"
signing:
credentials:
- private-key-location: classpath:sp-private.pem
certificate-location: classpath:sp-cert.crt
decryption:
credentials:
- private-key-location: classpath:sp-private.pem
certificate-location: classpath:sp-cert.crt
Spring crea automáticamente los endpoints /login/saml2/sso/{registrationId} (recibe la respuesta) y /saml2/authenticate/{registrationId} (inicia el login).
SecurityFilterChain con saml2Login
@Bean
SecurityFilterChain saml(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login/**", "/saml2/**", "/error").permitAll()
.anyRequest().authenticated())
.saml2Login(Customizer.withDefaults())
.saml2Logout(Customizer.withDefaults());
return http.build();
}
saml2Login() registra los filtros que procesan el SAMLResponse, validan la firma con el certificado del IdP, comprueban condiciones (audiencia, fecha, replay) y crean un Saml2Authentication.
saml2Logout() añade soporte para Single Logout (SLO), que cierra sesión en el IdP y en todas las apps federadas con la misma identidad.
Mapear atributos a autoridades
Por defecto, las autoridades del usuario son los nombres de los atributos del SAMLResponse. En Entra ID los más comunes son http://schemas.microsoft.com/ws/2008/06/identity/claims/role y los app roles definidos en la propia app registration.
Para un mapeo limpio, sustituimos el Saml2AuthenticatedPrincipal por uno propio mediante un Converter.
@Bean
SecurityFilterChain saml(HttpSecurity http,
Converter<ResponseToken, Saml2Authentication> converter) throws Exception {
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter(converter);
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.saml2Login(saml -> saml.authenticationManager(new ProviderManager(provider)));
return http.build();
}
@Bean
Converter<ResponseToken, Saml2Authentication> samlConverter() {
var delegate = OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter();
return token -> {
Saml2Authentication auth = delegate.convert(token);
Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) auth.getPrincipal();
Collection<GrantedAuthority> authorities = principal.getAttribute("roles")
.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toString().toUpperCase()))
.toList();
return new Saml2Authentication(principal, auth.getSaml2Response(), authorities);
};
}
Con esto, un usuario al que el IdP le envíe roles=admin,operador aparecerá en Spring con ROLE_ADMIN y ROLE_OPERADOR.
Configuración del lado IdP
El registro de la aplicación en el IdP exige tres URLs y el certificado del SP.
- Entity ID:
https://app.demo.local/saml2/sp - Reply URL (ACS):
https://app.demo.local/login/saml2/sso/entra - Sign-out URL:
https://app.demo.local/logout/saml2/slo - Certificado de firma del SP: el
.crtexportado con keytool.
En Entra ID, dentro del portal Azure, el flujo es Enterprise Applications, New Application, Non-gallery, configurar Single Sign-On (SAML), pegar las URLs y añadir el certificado.
En Okta, Applications, Create App Integration, SAML 2.0, rellenar metadatos del SP y mapear atributos en la pestaña Attributes.
Verifica siempre la firma de las assertions (
AssertionsSigned=true). Sin firma, un atacante con acceso a la red puede inyectar respuestas falsas.
Single Logout (SLO)
El SLO permite cerrar sesión en todas las aplicaciones federadas con un único click. Spring Security gestiona el envío del LogoutRequest y el procesamiento del LogoutResponse.
spring:
security:
saml2:
relyingparty:
registration:
entra:
singlelogout:
binding: POST
response-url: "{baseUrl}/logout/saml2/slo"
Y en la SecurityFilterChain:
.saml2Logout(slo -> slo
.logoutRequest(req -> req.logoutRequestResolver(customResolver))
.logoutResponse(res -> res.logoutResponseValidator(customValidator)))
SLO es complejo de implementar correctamente. Si tu app no es la única federada, considera no usar SLO y dejar que cada app cierre su propia sesión.
Validaciones críticas
Spring valida automáticamente, pero conviene saber qué se comprueba.
- 1. Firma del XML: el
ResponseoAssertiondebe estar firmado por el certificado declarado en la metadata del IdP. - 2. Condition
Audience: debe coincidir con el Entity ID del SP. - 3. Condition
NotOnOrAfteryNotBefore: la assertion solo es válida en una ventana temporal corta. - 4.
InResponseTo: el ID del response debe coincidir con el ID del request original (anti-replay). - 5.
Destination: la URL en la respuesta debe ser la del SP, no otra.
Desactivar cualquiera de estas validaciones, aunque sea por comodidad en desarrollo, abre la puerta a ataques de XSW (XML Signature Wrapping) documentados desde 2012.
Diferencias con OAuth2 / OIDC
| Aspecto | SAML 2.0 | OAuth2 / OIDC | |---|---|---| | Formato | XML firmado | JWT firmado | | Transporte típico | Form POST | Redirect con fragment / código | | Tamaño del mensaje | Grande (varios KB) | Compacto (cientos de bytes) | | Logout | Single Logout estandarizado | Back-channel logout (RP-Initiated) | | Casos típicos | Apps internas, B2B, sector público | APIs, SPAs, móvil | | Proveedores | Entra ID, Okta, ADFS, Ping | Google, GitHub, Auth0, Keycloak |
En arquitecturas mixtas se ven ambos: el portal corporativo usa SAML, mientras que las APIs y la app móvil usan OIDC. Spring Security soporta los dos en la misma aplicación con SecurityFilterChain distintos.
Errores frecuentes
- Reloj desincronizado: las condiciones temporales fallan si el reloj del SP difiere del IdP en más de unos segundos. Usa NTP.
- Metadata caducada: la URL de metadata del IdP debe ser accesible y la app debe refrescar el certificado periódicamente.
- Confundir Entity ID con URL: el Entity ID es un identificador, no necesariamente una URL accesible. Sigue la convención
https://dominio/saml2/sp. - No firmar el
AuthnRequest: aunque algunos IdP lo aceptan sin firmar, lo correcto es firmarlo con la clave del SP.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Security es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Security
Explora más contenido relacionado con Spring Security y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Configurar Spring Security SAML 2.0 Service Provider, integrar con Entra ID y Okta como Identity Provider y mapear atributos del SAMLResponse a roles y autoridades de Spring.