SAML2

Spring Security provides comprehensive SAML 2 support. This section discusses how to integrate SAML 2 into your servlet based application.

SAML 2.0 Login

The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users log in to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc).

SAML 2.0 Login is implemented by using the Web Browser SSO Profile, as specified in SAML 2 Profiles.

Since 2009, support for relying parties has existed as an extension project. In 2019, the process began to port that into Spring Security proper. This process is similar to the one started in 2017 for Spring Security’s OAuth 2.0 support.

A working sample for SAML 2.0 Login is available in the Spring Security Samples repository.

Let’s take a look at how SAML 2.0 Relying Party Authentication works within Spring Security. First, we see that, like OAuth 2.0 Login, Spring Security takes the user to a third-party for performing authentication. It does this through a series of redirects.

saml2webssoauthenticationrequestfilter
Figure 1. Redirecting to Asserting Party Authentication

The figure above builds off our SecurityFilterChain and AbstractAuthenticationProcessingFilter diagrams:

number 1 First, a user makes an unauthenticated request to the resource /private for which it is not authorized.

number 2 Spring Security’s FilterSecurityInterceptor indicates that the unauthenticated request is Denied by throwing an AccessDeniedException.

number 3 Since the user lacks authorization, the ExceptionTranslationFilter initiates Start Authentication. The configured AuthenticationEntryPoint is an instance of LoginUrlAuthenticationEntryPoint which redirects to the <saml2:AuthnRequest> generating endpoint, Saml2WebSsoAuthenticationRequestFilter. Or, if you’ve configured more than one asserting party, it will first redirect to a picker page.

number 4 Next, the Saml2WebSsoAuthenticationRequestFilter creates, signs, serializes, and encodes a <saml2:AuthnRequest> using its configured Saml2AuthenticationRequestFactory.

number 5 Then, the browser takes this <saml2:AuthnRequest> and presents it to the asserting party. The asserting party attempts to authentication the user. If successful, it will return a <saml2:Response> back to the browser.

number 6 The browser then POSTs the <saml2:Response> to the assertion consumer service endpoint.

saml2webssoauthenticationfilter
Figure 2. Authenticating a <saml2:Response>

The figure builds off our SecurityFilterChain diagram.

number 1 When the browser submits a <saml2:Response> to the application, it delegates to Saml2WebSsoAuthenticationFilter. This filter calls its configured AuthenticationConverter to create a Saml2AuthenticationToken by extracting the response from the HttpServletRequest. This converter additionally resolves the RelyingPartyRegistration and supplies it to Saml2AuthenticationToken.

number 2 Next, the filter passes the token to its configured AuthenticationManager. By default, it will use the OpenSAML authentication provider.

number 3 If authentication fails, then Failure

number 4 If authentication is successful, then Success.

  • The Authentication is set on the SecurityContextHolder.

  • The Saml2WebSsoAuthenticationFilter invokes FilterChain#doFilter(request,response) to continue with the rest of the application logic.

Minimal Dependencies

SAML 2.0 service provider support resides in spring-security-saml2-service-provider. It builds off of the OpenSAML library.

Minimal Configuration

When using Spring Boot, configuring an application as a service provider consists of two basic steps. First, include the needed dependencies and second, indicate the necessary asserting party metadata.

Also, this presupposes that you’ve already registered the relying party with your asserting party.

Specifying Identity Provider Metadata

In a Spring Boot application, to specify an identity provider’s metadata, simply do:

spring:
  security:
    saml2:
      relyingparty:
        registration:
          adfs:
            identityprovider:
              entity-id: https://idp.example.com/issuer
              verification.credentials:
                - certificate-location: "classpath:idp.crt"
              singlesignon.url: https://idp.example.com/issuer/sso
              singlesignon.sign-request: false

where

  • https://idp.example.com/issuer is the value contained in the Issuer attribute of the SAML responses that the identity provider will issue

  • classpath:idp.crt is the location on the classpath for the identity provider’s certificate for verifying SAML responses, and

  • https://idp.example.com/issuer/sso is the endpoint where the identity provider is expecting AuthnRequest s.

And that’s it!

Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party. These are frequently abbreviated as AP and RP, respectively.

Runtime Expectations

As configured above, the application processes any POST /login/saml2/sso/{registrationId} request containing a SAMLResponse parameter:

POST /login/saml2/sso/adfs HTTP/1.1

SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...

There are two ways to see induce your asserting party to generate a SAMLResponse:

  • First, you can navigate to your asserting party. It likely has some kind of link or button for each registered relying party that you can click to send the SAMLResponse.

  • Second, you can navigate to a protected page in your app, for example, http://localhost:8080. Your app then redirects to the configured asserting party which then sends the SAMLResponse.

From here, consider jumping to:

How SAML 2.0 Login Integrates with OpenSAML

Spring Security’s SAML 2.0 support has a couple of design goals:

  • First, rely on a library for SAML 2.0 operations and domain objects. To achieve this, Spring Security uses OpenSAML.

  • Second, ensure this library is not required when using Spring Security’s SAML support. To achieve this, any interfaces or classes where Spring Security uses OpenSAML in the contract remain encapsulated. This makes it possible for you to switch out OpenSAML for some other library or even an unsupported version of OpenSAML.

As a natural outcome of the above two goals, Spring Security’s SAML API is quite small relative to other modules. Instead, classes like OpenSaml4AuthenticationRequestFactory and OpenSaml4AuthenticationProvider expose Converter s that customize various steps in the authentication process.

For example, once your application receives a SAMLResponse and delegates to Saml2WebSsoAuthenticationFilter, the filter will delegate to OpenSaml4AuthenticationProvider.

For backward compatibility, Spring Security will use the latest OpenSAML 3 by default. Note, though that OpenSAML 3 has reached it’s end-of-life and updating to OpenSAML 4.x is recommended. For that reason, Spring Security supports both OpenSAML 3.x and 4.x. If you manage your OpenSAML dependency to 4.x, then Spring Security will select its OpenSAML 4.x implementations.
Authenticating an OpenSAML Response

opensamlauthenticationprovider

This figure builds off of the Saml2WebSsoAuthenticationFilter diagram.

number 1 The Saml2WebSsoAuthenticationFilter formulates the Saml2AuthenticationToken and invokes the AuthenticationManager.

number 2 The AuthenticationManager invokes the OpenSAML authentication provider.

number 3 The authentication provider deserializes the response into an OpenSAML Response and checks its signature. If the signature is invalid, authentication fails.

number 4 Then, the provider decrypts any EncryptedAssertion elements. If any decryptions fail, authentication fails.

number 5 Next, the provider validates the response’s Issuer and Destination values. If they don’t match what’s in the RelyingPartyRegistration, authentication fails.

number 6 After that, the provider verifies the signature of each Assertion. If any signature is invalid, authentication fails. Also, if neither the response nor the assertions have signatures, authentication fails. Either the response or all the assertions must have signatures.

number 7 Then, the provider decrypts any EncryptedID or EncryptedAttribute elements. If any decryptions fail, authentication fails.

number 8 Next, the provider validates each assertion’s ExpiresAt and NotBefore timestamps, the <Subject> and any <AudienceRestriction> conditions. If any validations fail, authentication fails.

number 9 Following that, the provider takes the first assertion’s AttributeStatement and maps it to a Map<String, List<Object>>. It also grants the ROLE_USER granted authority.

number 10 And finally, it takes the NameID from the first assertion, the Map of attributes, and the GrantedAuthority and constructs a Saml2AuthenticatedPrincipal. Then, it places that principal and the authorities into a Saml2Authentication.

The resulting Authentication#getPrincipal is a Spring Security Saml2AuthenticatedPrincipal object, and Authentication#getName maps to the first assertion’s NameID element.

Customizing OpenSAML Configuration

Any class that uses both Spring Security and OpenSAML should statically initialize OpenSamlInitializationService at the beginning of the class, like so:

Java
static {
	OpenSamlInitializationService.initialize();
}
Kotlin
companion object {
    init {
        OpenSamlInitializationService.initialize()
    }
}

This replaces OpenSAML’s InitializationService#initialize.

Occasionally, it can be valuable to customize how OpenSAML builds, marshalls, and unmarshalls SAML objects. In these circumstances, you may instead want to call OpenSamlInitializationService#requireInitialize(Consumer) that gives you access to OpenSAML’s XMLObjectProviderFactory.

For example, when sending an unsigned AuthNRequest, you may want to force reauthentication. In that case, you can register your own AuthnRequestMarshaller, like so:

Java
static {
    OpenSamlInitializationService.requireInitialize(factory -> {
        AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
            @Override
            public Element marshall(XMLObject object, Element element) throws MarshallingException {
                configureAuthnRequest((AuthnRequest) object);
                return super.marshall(object, element);
            }

            public Element marshall(XMLObject object, Document document) throws MarshallingException {
                configureAuthnRequest((AuthnRequest) object);
                return super.marshall(object, document);
            }

            private void configureAuthnRequest(AuthnRequest authnRequest) {
                authnRequest.setForceAuthn(true);
            }
        }

        factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
    });
}
Kotlin
companion object {
    init {
        OpenSamlInitializationService.requireInitialize {
            val marshaller = object : AuthnRequestMarshaller() {
                override fun marshall(xmlObject: XMLObject, element: Element): Element {
                    configureAuthnRequest(xmlObject as AuthnRequest)
                    return super.marshall(xmlObject, element)
                }

                override fun marshall(xmlObject: XMLObject, document: Document): Element {
                    configureAuthnRequest(xmlObject as AuthnRequest)
                    return super.marshall(xmlObject, document)
                }

                private fun configureAuthnRequest(authnRequest: AuthnRequest) {
                    authnRequest.isForceAuthn = true
                }
            }
            it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
        }
    }
}

The requireInitialize method may only be called once per application instance.

Overriding or Replacing Boot Auto Configuration

There are two @Bean s that Spring Boot generates for a relying party.

The first is a WebSecurityConfigurerAdapter that configures the app as a relying party. When including spring-security-saml2-service-provider, the WebSecurityConfigurerAdapter looks like:

Example 1. Default JWT Configuration
Java
protected void configure(HttpSecurity http) {
    http
        .authorizeRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .saml2Login(withDefaults());
}
Kotlin
fun configure(http: HttpSecurity) {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        saml2Login { }
    }
}

If the application doesn’t expose a WebSecurityConfigurerAdapter bean, then Spring Boot will expose the above default one.

You can replace this by exposing the bean within the application:

Example 2. Custom SAML 2.0 Login Configuration
Java
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(withDefaults());
    }
}
Kotlin
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("ROLE_USER"))
                authorize(anyRequest, authenticated)
            }
            saml2Login {
            }
        }
    }
}

The above requires the role of USER for any URL that starts with /messages/.

The second @Bean Spring Boot creates is a RelyingPartyRegistrationRepository, which represents the asserting party and relying party metadata. This includes things like the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.

You can override the default by publishing your own RelyingPartyRegistrationRepository bean. For example, you can look up the asserting party’s configuration by hitting its metadata endpoint like so:

Example 3. Relying Party Registration Repository
Java
@Value("${metadata.location}")
String assertingPartyMetadataLocation;

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
    RelyingPartyRegistration registration = RelyingPartyRegistrations
            .fromMetadataLocation(assertingPartyMetadataLocation)
            .registrationId("example")
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}
Kotlin
@Value("\${metadata.location}")
var assertingPartyMetadataLocation: String? = null

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
    val registration = RelyingPartyRegistrations
        .fromMetadataLocation(assertingPartyMetadataLocation)
        .registrationId("example")
        .build()
    return InMemoryRelyingPartyRegistrationRepository(registration)
}

Or you can provide each detail manually, as you can see below:

Example 4. Relying Party Registration Repository Manual Configuration
Java
@Value("${verification.key}")
File verificationKey;

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
    X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
    Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
    RelyingPartyRegistration registration = RelyingPartyRegistration
            .withRegistrationId("example")
            .assertingPartyDetails(party -> party
                .entityId("https://idp.example.com/issuer")
                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
                .wantAuthnRequestsSigned(false)
                .verificationX509Credentials(c -> c.add(credential))
            )
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}
Kotlin
@Value("\${verification.key}")
var verificationKey: File? = null

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
    val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
    val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
    val registration = RelyingPartyRegistration
        .withRegistrationId("example")
        .assertingPartyDetails { party: AssertingPartyDetails.Builder ->
            party
                .entityId("https://idp.example.com/issuer")
                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
                .wantAuthnRequestsSigned(false)
                .verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
                    c.add(
                        credential
                    )
                }
        }
        .build()
    return InMemoryRelyingPartyRegistrationRepository(registration)
}
Note that X509Support is an OpenSAML class, used here in the snippet for brevity

Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configured WebSecurityConfigurerAdapter:

Example 5. Custom Relying Party Registration DSL
Java
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .relyingPartyRegistrationRepository(relyingPartyRegistrations())
            );
    }
}
Kotlin
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("ROLE_USER"))
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                relyingPartyRegistrationRepository = relyingPartyRegistrations()
            }
        }
    }
}
A relying party can be multi-tenant by registering more than one relying party in the RelyingPartyRegistrationRepository.

RelyingPartyRegistration

A RelyingPartyRegistration instance represents a link between an relying party and assering party’s metadata.

In a RelyingPartyRegistration, you can provide relying party metadata like its Issuer value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads.

Also, you can provide asserting party metadata like its Issuer value, where it expects AuthnRequests to be sent to, and any public credentials that it owns for the purposes of the relying party verifying or encrypting payloads.

The following RelyingPartyRegistration is the minimum required for most setups:

Java
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
        .fromMetadataLocation("https://ap.example.org/metadata")
        .registrationId("my-id")
        .build();
Kotlin
val relyingPartyRegistration = RelyingPartyRegistrations
    .fromMetadataLocation("https://ap.example.org/metadata")
    .registrationId("my-id")
    .build()

Note that you can also create a RelyingPartyRegistration from an arbitrary InputStream source. One such example is when the metadata is stored in a database:

String xml = fromDatabase();
try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
            .fromMetadata(source)
            .registrationId("my-id")
            .build();
}

Though a more sophisticated setup is also possible, like so:

Java
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
        .entityId("{baseUrl}/{registrationId}")
        .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
        .assertingPartyDetails(party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        )
        .build();
Kotlin
val relyingPartyRegistration =
    RelyingPartyRegistration.withRegistrationId("my-id")
        .entityId("{baseUrl}/{registrationId}")
        .decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
            c.add(relyingPartyDecryptingCredential())
        }
        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
        .assertingPartyDetails { party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        }
        .build()
The top-level metadata methods are details about the relying party. The methods inside assertingPartyDetails are details about the asserting party.
The location where a relying party is expecting SAML Responses is the Assertion Consumer Service Location.

The default for the relying party’s entityId is {baseUrl}/saml2/service-provider-metadata/{registrationId}. This is this value needed when configuring the asserting party to know about your relying party.

The default for the assertionConsumerServiceLocation is /login/saml2/sso/{registrationId}. It’s mapped by default to Saml2WebSsoAuthenticationFilter in the filter chain.

URI Patterns

You probably noticed in the above examples the {baseUrl} and {registrationId} placeholders.

These are useful for generating URIs. As such, the relying party’s entityId and assertionConsumerServiceLocation support the following placeholders:

  • baseUrl - the scheme, host, and port of a deployed application

  • registrationId - the registration id for this relying party

  • baseScheme - the scheme of a deployed application

  • baseHost - the host of a deployed application

  • basePort - the port of a deployed application

For example, the assertionConsumerServiceLocation defined above was:

/my-login-endpoint/{registrationId}

which in a deployed application would translate to

/my-login-endpoint/adfs

The entityId above was defined as:

{baseUrl}/{registrationId}

which in a deployed application would translate to

https://rp.example.com/adfs

Credentials

You also likely noticed the credential that was used.

Oftentimes, a relying party will use the same key to sign payloads as well as decrypt them. Or it will use the same key to verify payloads as well as encrypt them.

Because of this, Spring Security ships with Saml2X509Credential, a SAML-specific credential that simplifies configuring the same key for different use cases.

At a minimum, it’s necessary to have a certificate from the asserting party so that the asserting party’s signed responses can be verified.

To construct a Saml2X509Credential that you’ll use to verify assertions from the asserting party, you can load the file and use the CertificateFactory like so:

Java
Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
    X509Certificate certificate = (X509Certificate)
            CertificateFactory.getInstance("X.509").generateCertificate(is);
    return Saml2X509Credential.verification(certificate);
}
Kotlin
val resource = ClassPathResource("ap.crt")
resource.inputStream.use {
    return Saml2X509Credential.verification(
        CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
    )
}

Let’s say that the asserting party is going to also encrypt the assertion. In that case, the relying party will need a private key to be able to decrypt the encrypted value.

In that case, you’ll need an RSAPrivateKey as well as its corresponding X509Certificate. You can load the first using Spring Security’s RsaKeyConverters utility class and the second as you did before:

Java
X509Certificate certificate = relyingPartyDecryptionCertificate();
Resource resource = new ClassPathResource("rp.crt");
try (InputStream is = resource.getInputStream()) {
    RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
    return Saml2X509Credential.decryption(rsa, certificate);
}
Kotlin
val certificate: X509Certificate = relyingPartyDecryptionCertificate()
val resource = ClassPathResource("rp.crt")
resource.inputStream.use {
    val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
    return Saml2X509Credential.decryption(rsa, certificate)
}
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.

Resolving the Relying Party from the Request

As seen so far, Spring Security resolves the RelyingPartyRegistration by looking for the registration id in the URI path.

There are a number of reasons you may want to customize. Among them:

  • You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme

  • You may identify tenants in a way other than by the URI path

To customize the way that a RelyingPartyRegistration is resolved, you can configure a custom Converter<HttpServletRequest, RelyingPartyRegistration>. The default looks up the registration id from the URI’s last path element and looks it up in your RelyingPartyRegistrationRepository.

You can provide a simpler resolver that, for example, always returns the same relying party:

Java
public class SingleRelyingPartyRegistrationResolver
        implements Converter<HttpServletRequest, RelyingPartyRegistration> {

    @Override
    public RelyingPartyRegistration convert(HttpServletRequest request) {
        return this.relyingParty;
    }
}
Kotlin
class SingleRelyingPartyRegistrationResolver : Converter<HttpServletRequest?, RelyingPartyRegistration?> {
    override fun convert(request: HttpServletRequest?): RelyingPartyRegistration? {
        return this.relyingParty
    }
}

Then, you can provide this resolver to the appropriate filters that produce <saml2:AuthnRequest> s, authenticate <saml2:Response> s, and produce <saml2:SPSSODescriptor> metadata.

Remember that if you have any placeholders in your RelyingPartyRegistration, your resolver implementation should resolve them.

Duplicated Relying Party Configurations

When an application uses multiple asserting parties, some configuration is duplicated between RelyingPartyRegistration instances:

  • The relying party’s entityId

  • Its assertionConsumerServiceLocation, and

  • Its credentials, for example its signing or decryption credentials

What’s nice about this setup is credentials may be more easily rotated for some identity providers vs others.

The duplication can be alleviated in a few different ways.

First, in YAML this can be alleviated with references, like so:

spring:
  security:
    saml2:
      relyingparty:
        okta:
          signing.credentials: &relying-party-credentials
            - private-key-location: classpath:rp.key
            - certificate-location: classpath:rp.crt
          identityprovider:
            entity-id: ...
        azure:
          signing.credentials: *relying-party-credentials
          identityprovider:
            entity-id: ...

Second, in a database, it’s not necessary to replicate RelyingPartyRegistration 's model.

Third, in Java, you can create a custom configuration method, like so:

Java
private RelyingPartyRegistration.Builder
        addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {

    Saml2X509Credential signingCredential = ...
    builder.signingX509Credentials(c -> c.addAll(signingCredential));
    // ... other relying party configurations
}

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
    RelyingPartyRegistration okta = addRelyingPartyDetails(
            RelyingPartyRegistrations
                .fromMetadataLocation(oktaMetadataUrl)
                .registrationId("okta")).build();

    RelyingPartyRegistration azure = addRelyingPartyDetails(
            RelyingPartyRegistrations
                .fromMetadataLocation(oktaMetadataUrl)
                .registrationId("azure")).build();

    return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
}
Kotlin
private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
    val signingCredential: Saml2X509Credential = ...
    builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
        c.add(
            signingCredential
        )
    }
    // ... other relying party configurations
}

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
    val okta = addRelyingPartyDetails(
        RelyingPartyRegistrations
            .fromMetadataLocation(oktaMetadataUrl)
            .registrationId("okta")
    ).build()
    val azure = addRelyingPartyDetails(
        RelyingPartyRegistrations
            .fromMetadataLocation(oktaMetadataUrl)
            .registrationId("azure")
    ).build()
    return InMemoryRelyingPartyRegistrationRepository(okta, azure)
}

Producing <saml2:AuthnRequest> s

As stated earlier, Spring Security’s SAML 2.0 support produces a <saml2:AuthnRequest> to commence authentication with the asserting party.

Spring Security achieves this in part by registering the Saml2WebSsoAuthenticationRequestFilter in the filter chain. This filter by default responds to endpoint /saml2/authenticate/{registrationId}.

For example, if you were deployed to https://rp.example.com and you gave your registration an ID of okta, you could navigate to:

and the result would be a redirect that included a SAMLRequest parameter containing the signed, deflated, and encoded <saml2:AuthnRequest>.

Changing How the <saml2:AuthnRequest> Gets Sent

By default, Spring Security signs each <saml2:AuthnRequest> and send it as a GET to the asserting party.

Many asserting parties don’t require a signed <saml2:AuthnRequest>. This can be configured automatically via RelyingPartyRegistrations, or you can supply it manually, like so:

Example 6. Not Requiring Signed AuthnRequests
Boot
spring:
  security:
    saml2:
      relyingparty:
        okta:
          identityprovider:
            entity-id: ...
            singlesignon.sign-request: false
Java
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
        // ...
        .assertingPartyDetails(party -> party
            // ...
            .wantAuthnRequestsSigned(false)
        )
        .build();
Kotlin
var relyingPartyRegistration: RelyingPartyRegistration =
    RelyingPartyRegistration.withRegistrationId("okta")
        // ...
        .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
                // ...
                .wantAuthnRequestsSigned(false)
        }
        .build();

Otherwise, you will need to specify a private key to RelyingPartyRegistration#signingX509Credentials so that Spring Security can sign the <saml2:AuthnRequest> before sending.

By default, Spring Security will sign the <saml2:AuthnRequest> using rsa-sha256, though some asserting parties will require a different algorithm, as indicated in their metadata.

You can configure the algorithm based on the asserting party’s metadata using RelyingPartyRegistrations.

Or, you can provide it manually:

Java
String metadataLocation = "classpath:asserting-party-metadata.xml";
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
        // ...
        .assertingPartyDetails((party) -> party
            // ...
            .signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
        )
        .build();
Kotlin
var metadataLocation = "classpath:asserting-party-metadata.xml"
var relyingPartyRegistration: RelyingPartyRegistration =
    RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
        // ...
        .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
                // ...
                .signingAlgorithms { sign: MutableList<String?> ->
                    sign.add(
                        SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512
                    )
                }
        }
        .build();
The snippet above uses the OpenSAML SignatureConstants class to supply the algorithm name. But, that’s just for convenience. Since the datatype is String, you can supply the name of the algorithm directly.

Some asserting parties require that the <saml2:AuthnRequest> be POSTed. This can be configured automatically via RelyingPartyRegistrations, or you can supply it manually, like so:

Java
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
        // ...
        .assertingPartyDetails(party -> party
            // ...
            .singleSignOnServiceBinding(Saml2MessageBinding.POST)
        )
        .build();
Kotlin
var relyingPartyRegistration: RelyingPartyRegistration? =
    RelyingPartyRegistration.withRegistrationId("okta")
        // ...
        .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
            // ...
            .singleSignOnServiceBinding(Saml2MessageBinding.POST)
        }
        .build()

Customizing OpenSAML’s AuthnRequest Instance

There are a number of reasons that you may want to adjust an AuthnRequest. For example, you may want ForceAuthN to be set to true, which Spring Security sets to false by default.

If you don’t need information from the HttpServletRequest to make your decision, then the easiest way is to register a custom AuthnRequestMarshaller with OpenSAML. This will give you access to post-process the AuthnRequest instance before it’s serialized.

But, if you do need something from the request, then you can use create a custom Saml2AuthenticationRequestContext implementation and then a Converter<Saml2AuthenticationRequestContext, AuthnRequest> to build an AuthnRequest yourself, like so:

Java
@Component
public class AuthnRequestConverter implements
        Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {

    private final AuthnRequestBuilder authnRequestBuilder;
    private final IssuerBuilder issuerBuilder;

    // ... constructor

    public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
        MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context;
        Issuer issuer = issuerBuilder.buildObject();
        issuer.setValue(myContext.getIssuer());

        AuthnRequest authnRequest = authnRequestBuilder.buildObject();
        authnRequest.setIssuer(issuer);
        authnRequest.setDestination(myContext.getDestination());
        authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl());

        // ... additional settings

        authRequest.setForceAuthn(myContext.getForceAuthn());
        return authnRequest;
    }
}
Kotlin
@Component
class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
    private val authnRequestBuilder: AuthnRequestBuilder? = null
    private val issuerBuilder: IssuerBuilder? = null

    // ... constructor
    override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
        val myContext: MySaml2AuthenticationRequestContext = context
        val issuer: Issuer = issuerBuilder.buildObject()
        issuer.value = myContext.getIssuer()
        val authnRequest: AuthnRequest = authnRequestBuilder.buildObject()
        authnRequest.issuer = issuer
        authnRequest.destination = myContext.getDestination()
        authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl()

        // ... additional settings
        authRequest.setForceAuthn(myContext.getForceAuthn())
        return authnRequest
    }
}

Then, you can construct your own Saml2AuthenticationRequestContextResolver and Saml2AuthenticationRequestFactory and publish them as @Bean s:

Java
@Bean
Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() {
    Saml2AuthenticationRequestContextResolver resolver =
            new DefaultSaml2AuthenticationRequestContextResolver();
    return request -> {
        Saml2AuthenticationRequestContext context = resolver.resolve(request);
        return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null);
    };
}

@Bean
Saml2AuthenticationRequestFactory authenticationRequestFactory(
        AuthnRequestConverter authnRequestConverter) {

    OpenSaml4AuthenticationRequestFactory authenticationRequestFactory =
            new OpenSaml4AuthenticationRequestFactory();
    authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
    return authenticationRequestFactory;
}
Kotlin
@Bean
open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver {
    val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver()
    return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest ->
        val context = resolver.resolve(request)
        MySaml2AuthenticationRequestContext(
            context,
            request.getParameter("force") != null
        )
    }
}

@Bean
open fun authenticationRequestFactory(
    authnRequestConverter: AuthnRequestConverter?
): Saml2AuthenticationRequestFactory? {
    val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory()
    authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter)
    return authenticationRequestFactory
}

Authenticating <saml2:Response> s

To verify SAML 2.0 Responses, Spring Security uses OpenSaml4AuthenticationProvider by default.

You can configure this in a number of ways including:

  1. Setting a clock skew to timestamp validation

  2. Mapping the response to a list of GrantedAuthority instances

  3. Customizing the strategy for validating assertions

  4. Customizing the strategy for decrypting response and assertion elements

To configure these, you’ll use the saml2Login#authenticationManager method in the DSL.

Setting a Clock Skew

It’s not uncommon for the asserting and relying parties to have system clocks that aren’t perfectly synchronized. For that reason, you can configure OpenSaml4AuthenticationProvider 's default assertion validator with some tolerance:

Java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidator(assertionToken -> {
                    Map<String, Object> params = new HashMap<>();
                    params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
                    // ... other validation parameters
                    return new ValidationContext(params);
                })
        );

        http
            .authorizeRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
    }
}
Kotlin
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setAssertionValidator(
            OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
                    val params: MutableMap<String, Any> = HashMap()
                    params[CLOCK_SKEW] =
                        Duration.ofMinutes(10).toMillis()
                    ValidationContext(params)
                })
        )
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
    }
}

Coordinating with a UserDetailsService

Or, perhaps you would like to include user details from a legacy UserDetailsService. In that case, the response authentication converter can come in handy, as can be seen below:

Java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
            Saml2Authentication authentication = OpenSaml4AuthenticationProvider
                    .createDefaultResponseAuthenticationConverter() (1)
                    .convert(responseToken);
            Assertion assertion = responseToken.getResponse().getAssertions().get(0);
            String username = assertion.getSubject().getNameID().getValue();
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); (2)
            return MySaml2Authentication(userDetails, authentication); (3)
        });

        http
            .authorizeRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
    }
}
Kotlin
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
    @Autowired
    var userDetailsService: UserDetailsService? = null

    override fun configure(http: HttpSecurity) {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
            val authentication = OpenSaml4AuthenticationProvider
                .createDefaultResponseAuthenticationConverter() (1)
                .convert(responseToken)
            val assertion: Assertion = responseToken.response.assertions[0]
            val username: String = assertion.subject.nameID.value
            val userDetails = userDetailsService!!.loadUserByUsername(username) (2)
            MySaml2Authentication(userDetails, authentication) (3)
        }
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
    }
}
1 First, call the default converter, which extracts attributes and authorities from the response
2 Second, call the UserDetailsService using the relevant information
3 Third, return a custom authentication that includes the user details
It’s not required to call OpenSaml4AuthenticationProvider 's default authentication converter. It returns a Saml2AuthenticatedPrincipal containing the attributes it extracted from AttributeStatement s as well as the single ROLE_USER authority.

Performing Additional Response Validation

OpenSaml4AuthenticationProvider validates the Issuer and Destination values right after decrypting the Response. You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.

For example, you can throw a custom exception with any additional information available in the Response object, like so:

OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseValidator((responseToken) -> {
	Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
		.createDefaultResponseValidator()
		.convert(responseToken)
		.concat(myCustomValidator.convert(responseToken));
	if (!result.getErrors().isEmpty()) {
		String inResponseTo = responseToken.getInResponseTo();
		throw new CustomSaml2AuthenticationException(result, inResponseTo);
	}
	return result;
});

Performing Additional Assertion Validation

OpenSaml4AuthenticationProvider performs minimal validation on SAML 2.0 Assertions. After verifying the signature, it will:

  1. Validate <AudienceRestriction> and <DelegationRestriction> conditions

  2. Validate <SubjectConfirmation> s, expect for any IP address information

To perform additional validation, you can configure your own assertion validator that delegates to OpenSaml4AuthenticationProvider 's default and then performs its own.

For example, you can use OpenSAML’s OneTimeUseConditionValidator to also validate a <OneTimeUse> condition, like so:

Java
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
provider.setAssertionValidator(assertionToken -> {
    Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
            .createDefaultAssertionValidator()
            .convert(assertionToken);
    Assertion assertion = assertionToken.getAssertion();
    OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
    ValidationContext context = new ValidationContext();
    try {
        if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
            return result;
        }
    } catch (Exception e) {
        return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
    }
    return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
});
Kotlin
var provider = OpenSaml4AuthenticationProvider()
var validator: OneTimeUseConditionValidator = ...
provider.setAssertionValidator { assertionToken ->
    val result = OpenSaml4AuthenticationProvider
        .createDefaultAssertionValidator()
        .convert(assertionToken)
    val assertion: Assertion = assertionToken.assertion
    val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
    val context = ValidationContext()
    try {
        if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
            return@setAssertionValidator result
        }
    } catch (e: Exception) {
        return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
    }
    result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
}
While recommended, it’s not necessary to call OpenSaml4AuthenticationProvider 's default assertion validator. A circumstance where you would skip it would be if you don’t need it to check the <AudienceRestriction> or the <SubjectConfirmation> since you are doing those yourself.

Customizing Decryption

Spring Security decrypts <saml2:EncryptedAssertion>, <saml2:EncryptedAttribute>, and <saml2:EncryptedID> elements automatically by using the decryption Saml2X509Credential instances registered in the RelyingPartyRegistration.

OpenSaml4AuthenticationProvider exposes two decryption strategies. The response decrypter is for decrypting encrypted elements of the <saml2:Response>, like <saml2:EncryptedAssertion>. The assertion decrypter is for decrypting encrypted elements of the <saml2:Assertion>, like <saml2:EncryptedAttribute> and <saml2:EncryptedID>.

You can replace OpenSaml4AuthenticationProvider’s default decryption strategy with your own. For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>, you can use it instead like so:

Java
MyDecryptionService decryptionService = ...;
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
Kotlin
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml4AuthenticationProvider()
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }

If you are also decrypting individual elements in a <saml2:Assertion>, you can customize the assertion decrypter, too:

Java
provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
Kotlin
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
There are two separate decrypters since assertions can be signed separately from responses. Trying to decrypt a signed assertion’s elements before signature verification may invalidate the signature. If your asserting party signs the response only, then it’s safe to decrypt all elements using only the response decrypter.

Using a Custom Authentication Manager

Of course, the authenticationManager DSL method can be also used to perform a completely custom SAML 2.0 authentication. This authentication manager should expect a Saml2AuthenticationToken object containing the SAML 2.0 Response XML data.

Java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(authenticationManager)
            )
        ;
    }
}
Kotlin
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = customAuthenticationManager
            }
        }
    }
}

Using Saml2AuthenticatedPrincipal

With the relying party correctly configured for a given asserting party, it’s ready to accept assertions. Once the relying party validates an assertion, the result is a Saml2Authentication with a Saml2AuthenticatedPrincipal.

This means that you can access the principal in your controller like so:

Java
@Controller
public class MainController {
	@GetMapping("/")
	public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
		String email = principal.getFirstAttribute("email");
		model.setAttribute("email", email);
		return "index";
	}
}
Kotlin
@Controller
class MainController {
    @GetMapping("/")
    fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
        val email = principal.getFirstAttribute<String>("email")
        model.setAttribute("email", email)
        return "index"
    }
}
Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call getAttribute to get the list of attributes or getFirstAttribute to get the first in the list. getFirstAttribute is quite handy when you know that there is only one value.

Producing <saml2:SPSSODescriptor> Metadata

You can publish a metadata endpoint by adding the Saml2MetadataFilter to the filter chain, as you’ll see below:

Java
Converter<HttpServletRequest, RelyingPartyRegistration> relyingPartyRegistrationResolver =
        new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
Saml2MetadataFilter filter = new Saml2MetadataFilter(
        relyingPartyRegistrationResolver,
        new OpenSamlMetadataResolver());

http
    // ...
    .saml2Login(withDefaults())
    .addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
Kotlin
val relyingPartyRegistrationResolver: Converter<HttpServletRequest, RelyingPartyRegistration> =
    DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)
val filter = Saml2MetadataFilter(
    relyingPartyRegistrationResolver,
    OpenSamlMetadataResolver()
)

http {
    //...
    saml2Login { }
    addFilterBefore<Saml2WebSsoAuthenticationFilter>(filter)
}

You can use this metadata endpoint to register your relying party with your asserting party. This is often as simple as finding the correct form field to supply the metadata endpoint.

By default, the metadata endpoint is /saml2/service-provider-metadata/{registrationId}. You can change this by calling the setRequestMatcher method on the filter:

Java
filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
Kotlin
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"))

ensuring that the registrationId hint is at the end of the path.

Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a registrationId hint, like so:

Java
filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
Kotlin
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))

Performing Single Logout

Spring Security does not yet support single logout.

Generally speaking, though, you can achieve this by creating and registering a custom LogoutSuccessHandler and RequestMatcher:

Java
http
    // ...
    .logout(logout -> logout
        .logoutSuccessHandler(myCustomSuccessHandler())
        .logoutRequestMatcher(myRequestMatcher())
    )
Kotlin
http {
    logout {
        // ...
        logoutSuccessHandler = myCustomSuccessHandler()
        logoutRequestMatcher = myRequestMatcher()
    }
}

The success handler will send logout requests to the asserting party.

The request matcher will detect logout requests from the asserting party.