Spring Security provides comprehensive SAML 2 support.
This section discusses how to integrate SAML 2 into your servlet based application.
[[servlet-saml2login]]
== 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 https://wiki.shibboleth.net/confluence/display/CONCEPT/FlowsAndConfig[log in] to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc).
NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in
Since 2009, support for relying parties has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project].
In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper.
A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[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 xref:servlet/oauth2/oauth2-login.adoc#oauth2login[ OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication.
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] and xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[ `AbstractAuthenticationProcessingFilter`] diagrams:
image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`.
image:{icondir}/number_3.png[] Since the user lacks authorization, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__.
The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`] which redirects to <<servlet-saml2login-sp-initiated-factory,the `<saml2:AuthnRequest>` generating endpoint>>, `Saml2WebSsoAuthenticationRequestFilter`.
Or, if you've <<servlet-saml2login-relyingpartyregistrationrepository,configured more than one asserting party>>, it will first redirect to a picker page.
image:{icondir}/number_4.png[] Next, the `Saml2WebSsoAuthenticationRequestFilter` creates, signs, serializes, and encodes a `<saml2:AuthnRequest>` using its configured <<servlet-saml2login-sp-initiated-factory,`Saml2AuthenticationRequestFactory`>>.
image:{icondir}/number_5.png[] 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.
image:{icondir}/number_6.png[] The browser then POSTs the `<saml2:Response>` to the assertion consumer service endpoint.
image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it <<servlet-saml2login-authenticate-responses, 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 <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
image:{icondir}/number_2.png[] Next, the filter passes the token to its configured xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`].
* The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[ `SecurityContextHolder`] is cleared out.
* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is invoked to restart the authentication process.
* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[ `Authentication`] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[ `SecurityContextHolder`].
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:
* <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
* <<servlet-saml2login-authenticatedprincipal,How to Use the `Saml2AuthenticatedPrincipal`>>
* <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
[[servlet-saml2login-architecture]]
=== 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`.
[NOTE]
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.
image:{icondir}/number_1.png[] The `Saml2WebSsoAuthenticationFilter` formulates the `Saml2AuthenticationToken` and invokes the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`].
image:{icondir}/number_2.png[] The xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`] invokes the OpenSAML authentication provider.
image:{icondir}/number_3.png[] The authentication provider deserializes the response into an OpenSAML `Response` and checks its signature.
If the signature is invalid, authentication fails.
image:{icondir}/number_4.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedAssertion` elements>>.
If any decryptions fail, authentication fails.
image:{icondir}/number_5.png[] Next, the provider validates the response's `Issuer` and `Destination` values.
If they don't match what's in the `RelyingPartyRegistration`, authentication fails.
image:{icondir}/number_6.png[] 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.
image:{icondir}/number_7.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedID` or `EncryptedAttribute` elements>>.
If any decryptions fail, authentication fails.
image:{icondir}/number_8.png[] Next, the provider validates each assertion's `ExpiresAt` and `NotBefore` timestamps, the `<Subject>` and any `<AudienceRestriction>` conditions.
If any validations fail, authentication fails.
image:{icondir}/number_9.png[] 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.
image:{icondir}/number_10.png[] 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.
[[servlet-saml2login-opensaml-customization]]
==== 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
[source,java,role="primary"]
----
static {
OpenSamlInitializationService.initialize();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
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:
The second `@Bean` Spring Boot creates is a {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.html[`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:
.Relying Party Registration Repository
====
.Java
[source,java,role="primary"]
----
@Value("${metadata.location}")
String assertingPartyMetadataLocation;
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`.
[[servlet-saml2login-relyingpartyregistration]]
=== RelyingPartyRegistration
A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`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:
The top-level metadata methods are details about the relying party.
The methods inside `assertingPartyDetails` are details about the asserting party.
[NOTE]
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 <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
[[servlet-saml2login-rpr-uripatterns]]
==== 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+`
[[servlet-saml2login-rpr-credentials]]
==== 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
[source,java,role="primary"]
----
Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
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
[source,java,role="primary"]
----
public class SingleRelyingPartyRegistrationResolver
public RelyingPartyRegistration convert(HttpServletRequest request) {
return this.relyingParty;
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
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 <<servlet-saml2login-sp-initiated-factory, produce `<saml2:AuthnRequest>` s>>, <<servlet-saml2login-authenticate-responses, authenticate `<saml2:Response>` s>>, and <<servlet-saml2login-metadata, produce `<saml2:SPSSODescriptor>` metadata>>.
[NOTE]
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
[[servlet-saml2login-rpr-duplicated]]
==== 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:
[source,yaml]
----
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:
.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 <<servlet-saml2login-relyingpartyregistrationrepository,metadata using `RelyingPartyRegistrations`>>.
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 <<servlet-saml2login-opensaml-customization,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:
<2> Second, call the xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[ `UserDetailsService`] using the relevant information
<3> Third, return a custom authentication that includes the user details
[NOTE]
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.
`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:
[source,java]
----
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
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.
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.
Spring Security decrypts `<saml2:EncryptedAssertion>`, `<saml2:EncryptedAttribute>`, and `<saml2:EncryptedID>` elements automatically by using the decryption <<servlet-saml2login-rpr-credentials,`Saml2X509Credential` instances>> registered in the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>.
fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
val email = principal.getFirstAttribute<String>("email")
model.setAttribute("email", email)
return "index"
}
}
----
====
[TIP]
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.
[[servlet-saml2login-metadata]]
=== Producing `<saml2:SPSSODescriptor>` Metadata
You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
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: