SSO empresarial con SAML 2.0: Entra ID, Okta y Spring Security

Avanzado
Spring Security
Spring Security
Actualizado: 19/04/2026

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, AuthnStatement y AttributeStatement.

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 .crt exportado 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 Response o Assertion debe estar firmado por el certificado declarado en la metadata del IdP.
  • 2. Condition Audience: debe coincidir con el Entity ID del SP.
  • 3. Condition NotOnOrAfter y NotBefore: 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 - Autor del tutorial

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.