2021-10-29 15:06:54 -06:00
= SAML 2.0 Login Overview
:figures: servlet/saml2
:icondir: icons
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
We start by examining how SAML 2.0 Relying Party Authentication works within Spring Security.
First, we see that, like <<oauth2login, OAuth 2.0 Login>>, Spring Security takes the user to a third party for performing authentication.
It does this through a series of redirects:
2020-08-28 12:42:44 -06:00
.Redirecting to Asserting Party Authentication
image::{figures}/saml2webssoauthenticationrequestfilter.png[]
2021-12-13 16:57:36 -06:00
[NOTE]
====
2021-10-05 22:36:03 +02:00
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] and xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[`AbstractAuthenticationProcessingFilter`] diagrams:
2021-12-13 16:57:36 -06:00
====
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the `/private` resource, for which it is not authorized.
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
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`.
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
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`.
Alternatively, if you have <<servlet-saml2login-relyingpartyregistrationrepository,configured more than one asserting party>>, it first redirects to a picker page.
2020-08-28 12:42:44 -06:00
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`>>.
2021-12-13 16:57:36 -06:00
image:{icondir}/number_5.png[] Then the browser takes this `<saml2:AuthnRequest>` and presents it to the asserting party.
The asserting party tries to authentication the user.
If successful, it returns a `<saml2:Response>` back to the browser.
2020-08-28 12:42:44 -06:00
image:{icondir}/number_6.png[] The browser then POSTs the `<saml2:Response>` to the assertion consumer service endpoint.
2021-12-13 16:57:36 -06:00
The following image shows how Spring Security authenticates a `<saml2:Response>`.
2020-08-28 12:42:44 -06:00
[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
.Authenticating a `<saml2:Response>`
image::{figures}/saml2webssoauthenticationfilter.png[]
2021-12-13 16:57:36 -06:00
[NOTE]
====
2021-10-05 22:36:03 +02:00
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
2021-12-13 16:57:36 -06:00
====
2020-08-28 12:42:44 -06:00
2021-10-29 15:06:54 -06:00
image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[delegates to `Saml2WebSsoAuthenticationFilter`].
2020-08-28 12:42:44 -06:00
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`.
2021-10-05 22:36:03 +02:00
image:{icondir}/number_2.png[] Next, the filter passes the token to its configured xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`].
2021-12-13 16:57:36 -06:00
By default, it uses the <<servlet-saml2login-architecture,`OpenSamlAuthenticationProvider`>>.
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
image:{icondir}/number_3.png[] If authentication fails, then _Failure_.
2020-08-28 12:42:44 -06:00
2021-10-05 22:36:03 +02:00
* 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.
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
image:{icondir}/number_4.png[] If authentication is successful, then _Success_.
2020-08-28 12:42:44 -06:00
2021-10-05 22:36:03 +02:00
* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`].
2020-08-28 12:42:44 -06:00
* The `Saml2WebSsoAuthenticationFilter` invokes `FilterChain#doFilter(request,response)` to continue with the rest of the application logic.
2020-04-06 14:36:33 -06:00
[[servlet-saml2login-minimaldependencies]]
2021-10-29 11:29:35 -06:00
== Minimal Dependencies
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`.
2020-04-06 14:36:33 -06:00
It builds off of the OpenSAML library.
[[servlet-saml2login-minimalconfiguration]]
2021-10-29 11:29:35 -06:00
== Minimal Configuration
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a service provider consists of two basic steps:
. Include the needed dependencies.
. Indicate the necessary asserting party metadata.
2020-08-28 12:42:44 -06:00
[NOTE]
2021-12-13 16:57:36 -06:00
Also, this configuration presupposes that you have already xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[registered the relying party with your asserting party].
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
[[saml2-specifying-identity-provider-metadata]]
2021-10-29 11:29:35 -06:00
=== Specifying Identity Provider Metadata
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
In a Spring Boot application, to specify an identity provider's metadata, create configuration similar to the following:
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
====
2020-04-06 14:36:33 -06:00
[source,yml]
2019-09-28 12:19:03 -07:00
----
2020-04-06 14:36:33 -06:00
spring:
security:
saml2:
relyingparty:
registration:
2020-08-28 12:42:44 -06:00
adfs:
2020-04-06 14:36:33 -06:00
identityprovider:
entity-id: https://idp.example.com/issuer
verification.credentials:
- certificate-location: "classpath:idp.crt"
2020-08-28 12:42:44 -06:00
singlesignon.url: https://idp.example.com/issuer/sso
2020-04-06 14:36:33 -06:00
singlesignon.sign-request: false
----
2021-12-13 16:57:36 -06:00
====
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
where:
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
* `https://idp.example.com/issuer` is the value contained in the `Issuer` attribute of the SAML responses that the identity provider issues.
* `classpath:idp.crt` is the location on the classpath for the identity provider's certificate for verifying SAML responses.
* `https://idp.example.com/issuer/sso` is the endpoint where the identity provider is expecting `AuthnRequest` instances.
2021-03-02 07:54:18 -07:00
* `adfs` is <<servlet-saml2login-relyingpartyregistrationid, an arbitrary identifier you choose>>
2020-04-06 14:36:33 -06:00
And that's it!
2020-08-28 12:42:44 -06:00
[NOTE]
2021-12-13 16:57:36 -06:00
====
2020-08-28 12:42:44 -06:00
Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party.
These are frequently abbreviated as AP and RP, respectively.
2021-12-13 16:57:36 -06:00
====
2020-08-28 12:42:44 -06:00
2021-10-29 11:29:35 -06:00
=== Runtime Expectations
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
As configured <<saml2-specifying-identity-provider-metadata,earlier>>, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
====
[source,http]
2020-08-28 12:42:44 -06:00
----
POST /login/saml2/sso/adfs HTTP/1.1
SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
----
2021-12-13 16:57:36 -06:00
====
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
There are two ways to induce your asserting party to generate a `SAMLResponse`:
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
* You can navigate to your asserting party.
2020-08-28 12:42:44 -06:00
It likely has some kind of link or button for each registered relying party that you can click to send the `SAMLResponse`.
2021-12-13 16:57:36 -06:00
* You can navigate to a protected page in your application -- for example, `http://localhost:8080`.
Your application then redirects to the configured asserting party, which then sends the `SAMLResponse`.
2020-08-28 12:42:44 -06:00
2020-04-06 14:36:33 -06:00
From here, consider jumping to:
2020-08-28 12:42:44 -06:00
* <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
2021-10-29 15:06:54 -06:00
* xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticatedprincipal[How to Use the `Saml2AuthenticatedPrincipal`]
2020-08-28 12:42:44 -06:00
* <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
2020-04-06 14:36:33 -06:00
[[servlet-saml2login-architecture]]
2021-10-29 11:29:35 -06:00
== How SAML 2.0 Login Integrates with OpenSAML
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
Spring Security's SAML 2.0 support has a couple of design goals:
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
* Rely on a library for SAML 2.0 operations and domain objects.
2020-08-28 12:42:44 -06:00
To achieve this, Spring Security uses OpenSAML.
2021-12-13 16:57:36 -06:00
* Ensure that this library is not required when using Spring Security's SAML support.
2020-08-28 12:42:44 -06:00
To achieve this, any interfaces or classes where Spring Security uses OpenSAML in the contract remain encapsulated.
2021-12-13 16:57:36 -06:00
This makes it possible for you to switch out OpenSAML for some other library or an unsupported version of OpenSAML.
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
As a natural outcome of these two goals, Spring Security's SAML API is quite small relative to other modules.
Instead, such classes as `OpenSamlAuthenticationRequestFactory` and `OpenSamlAuthenticationProvider` expose `Converter` implementationss that customize various steps in the authentication process.
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter delegates to `OpenSamlAuthenticationProvider`:
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
.Authenticating an OpenSAML `Response`
image:{figures}/opensamlauthenticationprovider.png[]
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
This figure builds off of the <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter` diagram>>.
2019-09-28 12:19:03 -07:00
2021-10-05 22:36:03 +02:00
image:{icondir}/number_1.png[] The `Saml2WebSsoAuthenticationFilter` formulates the `Saml2AuthenticationToken` and invokes the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`].
2020-04-06 14:36:33 -06:00
2021-10-05 22:36:03 +02:00
image:{icondir}/number_2.png[] The xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`] invokes the OpenSAML authentication provider.
2020-08-28 12:42:44 -06:00
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.
2021-12-13 16:57:36 -06:00
image:{icondir}/number_4.png[] Then the provider xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-opensamlauthenticationprovider-decryption[decrypts any `EncryptedAssertion` elements].
2020-08-28 12:42:44 -06:00
If any decryptions fail, authentication fails.
2020-10-02 16:47:44 -06:00
image:{icondir}/number_5.png[] Next, the provider validates the response's `Issuer` and `Destination` values.
2021-12-13 16:57:36 -06:00
If they do not match what's in the `RelyingPartyRegistration`, authentication fails.
2020-10-02 16:47:44 -06:00
2020-08-28 12:42:44 -06:00
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.
2020-10-02 16:47:44 -06:00
Either the response or all the assertions must have signatures.
2020-08-28 12:42:44 -06:00
2021-10-29 15:06:54 -06:00
image:{icondir}/number_7.png[] Then, the provider xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-opensamlauthenticationprovider-decryption[,]decrypts any `EncryptedID` or `EncryptedAttribute` elements].
2020-10-02 16:47:44 -06:00
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.
2020-08-28 12:42:44 -06:00
If any validations fail, authentication fails.
2020-10-02 16:47:44 -06:00
image:{icondir}/number_9.png[] Following that, the provider takes the first assertion's `AttributeStatement` and maps it to a `Map<String, List<Object>>`.
2020-08-28 12:42:44 -06:00
It also grants the `ROLE_USER` granted authority.
2020-10-02 16:47:44 -06:00
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`.
2020-08-28 12:42:44 -06:00
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.
2021-03-02 07:54:18 -07:00
`Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId` holds the <<servlet-saml2login-relyingpartyregistrationid,identifier to the associated `RelyingPartyRegistration`>>.
2020-08-28 12:42:44 -06:00
[[servlet-saml2login-opensaml-customization]]
2021-10-29 11:29:35 -06:00
=== Customizing OpenSAML Configuration
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class:
2020-08-28 12:42:44 -06:00
2021-06-16 10:31:29 +02:00
====
.Java
[source,java,role="primary"]
2020-08-28 12:42:44 -06:00
----
static {
OpenSamlInitializationService.initialize();
}
----
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
companion object {
init {
OpenSamlInitializationService.initialize()
}
}
----
====
2020-08-28 12:42:44 -06:00
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`.
2021-05-14 10:36:00 -06:00
For example, when sending an unsigned AuthNRequest, you may want to force reauthentication.
In that case, you can register your own `AuthnRequestMarshaller`, like so:
2020-08-28 12:42:44 -06:00
2021-06-16 10:31:29 +02:00
====
.Java
[source,java,role="primary"]
2020-08-28 12:42:44 -06:00
----
static {
2021-06-28 12:58:57 -06:00
OpenSamlInitializationService.requireInitialize(factory -> {
AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
@Override
2020-08-28 12:42:44 -06:00
public Element marshall(XMLObject object, Element element) throws MarshallingException {
2021-06-28 12:58:57 -06:00
configureAuthnRequest((AuthnRequest) object);
return super.marshall(object, element);
2020-08-28 12:42:44 -06:00
}
public Element marshall(XMLObject object, Document document) throws MarshallingException {
2021-06-28 12:58:57 -06:00
configureAuthnRequest((AuthnRequest) object);
return super.marshall(object, document);
2020-08-28 12:42:44 -06:00
}
private void configureAuthnRequest(AuthnRequest authnRequest) {
2021-06-28 12:58:57 -06:00
authnRequest.setForceAuthn(true);
2020-08-28 12:42:44 -06:00
}
2021-06-28 12:58:57 -06:00
}
2021-05-14 10:36:00 -06:00
2021-06-28 12:58:57 -06:00
factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
});
2020-08-28 12:42:44 -06:00
}
----
2020-04-06 14:36:33 -06:00
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
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)
}
}
}
----
====
2021-12-13 16:57:36 -06:00
The `requireInitialize` method may be called only once per application instance.
2020-04-06 14:36:33 -06:00
[[servlet-saml2login-sansboot]]
2021-10-29 11:29:35 -06:00
== Overriding or Replacing Boot Auto Configuration
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
Spring Boot generates two `@Bean` objects for a relying party.
2020-04-06 14:36:33 -06:00
2022-02-08 16:12:10 +01:00
The first is a `SecurityFilterChain` that configures the application as a relying party.
When including `spring-security-saml2-service-provider`, the `SecurityFilterChain` looks like:
2020-04-06 14:36:33 -06:00
.Default JWT Configuration
====
.Java
[source,java,role="primary"]
----
2022-02-08 16:12:10 +01:00
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
2020-04-06 14:36:33 -06:00
http
2021-11-10 15:15:11 -07:00
.authorizeHttpRequests(authorize -> authorize
2020-04-06 14:36:33 -06:00
.anyRequest().authenticated()
)
.saml2Login(withDefaults());
2022-02-08 16:12:10 +01:00
return http.build();
2020-04-06 14:36:33 -06:00
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
2022-02-08 16:12:10 +01:00
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
2020-04-06 14:36:33 -06:00
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login { }
2019-09-28 12:19:03 -07:00
}
2022-02-08 16:12:10 +01:00
return http.build()
2020-04-06 14:36:33 -06:00
}
----
====
2019-09-28 12:19:03 -07:00
2022-02-08 16:12:10 +01:00
If the application does not expose a `SecurityFilterChain` bean, Spring Boot exposes the preceding default one.
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
You can replace this by exposing the bean within the application:
2020-04-06 14:36:33 -06:00
.Custom SAML 2.0 Login Configuration
====
.Java
[source,java,role="primary"]
----
2022-07-30 03:47:02 +02:00
@Configuration
2020-04-06 14:36:33 -06:00
@EnableWebSecurity
2022-02-08 16:12:10 +01:00
public class MyCustomSecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
2019-09-28 12:19:03 -07:00
http
2021-11-10 15:15:11 -07:00
.authorizeHttpRequests(authorize -> authorize
2020-04-06 14:36:33 -06:00
.mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
2020-01-10 13:10:36 +01:00
.anyRequest().authenticated()
2019-12-30 17:49:35 +01:00
)
2020-04-06 14:36:33 -06:00
.saml2Login(withDefaults());
2022-02-08 16:12:10 +01:00
return http.build();
2019-09-28 12:19:03 -07:00
}
}
----
2020-04-06 14:36:33 -06:00
.Kotlin
[source,kotlin,role="secondary"]
2019-09-28 12:19:03 -07:00
----
2022-07-30 03:47:02 +02:00
@Configuration
2019-09-28 12:19:03 -07:00
@EnableWebSecurity
2022-02-08 16:12:10 +01:00
class MyCustomSecurityConfiguration {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
2020-04-06 14:36:33 -06:00
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("ROLE_USER"))
authorize(anyRequest, authenticated)
}
saml2Login {
}
}
2022-02-08 16:12:10 +01:00
return http.build()
2020-04-06 14:36:33 -06:00
}
}
----
====
2019-09-28 12:19:03 -07:00
2021-12-13 16:57:36 -06:00
The preceding example requires the role of `USER` for any URL that starts with `/messages/`.
2020-04-06 14:36:33 -06:00
[[servlet-saml2login-relyingpartyregistrationrepository]]
2021-07-09 11:08:39 -03:00
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.
2021-12-13 16:57:36 -06:00
This includes such things as the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.
2020-04-06 14:36:33 -06:00
You can override the default by publishing your own `RelyingPartyRegistrationRepository` bean.
2021-12-13 16:57:36 -06:00
For example, you can look up the asserting party's configuration by hitting its metadata endpoint:
2020-04-06 14:36:33 -06:00
.Relying Party Registration Repository
====
2021-06-16 10:31:29 +02:00
.Java
[source,java,role="primary"]
2020-04-06 14:36:33 -06:00
----
@Value("${metadata.location}")
String assertingPartyMetadataLocation;
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
2021-06-28 12:58:57 -06:00
RelyingPartyRegistration registration = RelyingPartyRegistrations
2020-04-06 14:36:33 -06:00
.fromMetadataLocation(assertingPartyMetadataLocation)
.registrationId("example")
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
----
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
@Value("\${metadata.location}")
var assertingPartyMetadataLocation: String? = null
@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
val registration = RelyingPartyRegistrations
.fromMetadataLocation(assertingPartyMetadataLocation)
.registrationId("example")
.build()
return InMemoryRelyingPartyRegistrationRepository(registration)
}
----
2020-04-06 14:36:33 -06:00
====
2021-03-02 07:54:18 -07:00
[[servlet-saml2login-relyingpartyregistrationid]]
[NOTE]
The `registrationId` is an arbitrary value that you choose for differentiating between registrations.
2021-12-13 16:57:36 -06:00
Alternatively, you can provide each detail manually:
2020-04-06 14:36:33 -06:00
.Relying Party Registration Repository Manual Configuration
====
2021-06-16 10:31:29 +02:00
.Java
[source,java,role="primary"]
2020-04-06 14:36:33 -06:00
----
@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);
}
----
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
@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)
}
----
2020-04-06 14:36:33 -06:00
====
[NOTE]
2021-12-13 16:57:36 -06:00
====
`X509Support` is an OpenSAML class, used in the preceding snippet for brevity.
====
2020-04-06 14:36:33 -06:00
[[servlet-saml2login-relyingpartyregistrationrepository-dsl]]
2022-02-08 16:12:10 +01:00
Alternatively, you can directly wire up the repository by using the DSL, which also overrides the auto-configured `SecurityFilterChain`:
2020-04-06 14:36:33 -06:00
.Custom Relying Party Registration DSL
====
.Java
[source,java,role="primary"]
----
2022-07-30 03:47:02 +02:00
@Configuration
2020-04-06 14:36:33 -06:00
@EnableWebSecurity
2022-02-08 16:12:10 +01:00
public class MyCustomSecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
2019-09-28 12:19:03 -07:00
http
2021-11-10 15:15:11 -07:00
.authorizeHttpRequests(authorize -> authorize
2020-04-06 14:36:33 -06:00
.mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
2020-01-10 13:10:36 +01:00
.anyRequest().authenticated()
2019-12-30 17:49:35 +01:00
)
2020-01-10 13:10:36 +01:00
.saml2Login(saml2 -> saml2
2020-04-06 14:36:33 -06:00
.relyingPartyRegistrationRepository(relyingPartyRegistrations())
);
2022-02-08 16:12:10 +01:00
return http.build();
2020-04-06 14:36:33 -06:00
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
2022-07-30 03:47:02 +02:00
@Configuration
2020-04-06 14:36:33 -06:00
@EnableWebSecurity
2022-02-08 16:12:10 +01:00
class MyCustomSecurityConfiguration {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
2020-04-06 14:36:33 -06:00
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("ROLE_USER"))
authorize(anyRequest, authenticated)
}
saml2Login {
relyingPartyRegistrationRepository = relyingPartyRegistrations()
}
}
2022-02-08 16:12:10 +01:00
return http.build()
2019-09-28 12:19:03 -07:00
}
}
----
2020-04-06 14:36:33 -06:00
====
[NOTE]
2021-12-13 16:57:36 -06:00
====
2020-04-06 14:36:33 -06:00
A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`.
2021-12-13 16:57:36 -06:00
====
2019-09-28 12:19:03 -07:00
2020-08-28 12:42:44 -06:00
[[servlet-saml2login-relyingpartyregistration]]
2021-10-29 11:29:35 -06:00
== RelyingPartyRegistration
2021-07-09 11:08:39 -03:00
A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
2021-12-13 16:57:36 -06:00
instance represents a link between an relying party and an asserting party's metadata.
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
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.
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
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.
2020-04-06 14:36:33 -06:00
The following `RelyingPartyRegistration` is the minimum required for most setups:
2021-06-16 10:31:29 +02:00
====
.Java
[source,java,role="primary"]
2020-04-06 14:36:33 -06:00
----
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("my-id")
.build();
----
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
val relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata")
.registrationId("my-id")
.build()
----
====
2021-05-14 13:46:29 -03:00
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:
[source,java]
----
String xml = fromDatabase();
try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
.fromMetadata(source)
.registrationId("my-id")
.build();
}
----
2021-12-13 16:57:36 -06:00
A more sophisticated setup is also possible:
2020-04-06 14:36:33 -06:00
2021-06-16 10:31:29 +02:00
====
.Java
[source,java,role="primary"]
2020-04-06 14:36:33 -06:00
----
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
.entityId("{baseUrl}/{registrationId}")
.decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
.assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
2021-06-08 15:28:40 +03:00
.assertingPartyDetails(party -> party
2020-04-06 14:36:33 -06:00
.entityId("https://ap.example.org")
.verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
.singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
2021-06-08 15:28:40 +03:00
)
.build();
2020-04-06 14:36:33 -06:00
----
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
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()
----
====
2020-04-06 14:36:33 -06:00
[TIP]
2021-12-13 16:57:36 -06:00
====
2020-08-28 12:42:44 -06:00
The top-level metadata methods are details about the relying party.
The methods inside `assertingPartyDetails` are details about the asserting party.
2021-12-13 16:57:36 -06:00
====
2020-04-06 14:36:33 -06:00
[NOTE]
2021-12-13 16:57:36 -06:00
====
2020-08-28 12:42:44 -06:00
The location where a relying party is expecting SAML Responses is the Assertion Consumer Service Location.
2021-12-13 16:57:36 -06:00
====
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
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.
2020-04-06 14:36:33 -06:00
The default for the `assertionConsumerServiceLocation` is `+/login/saml2/sso/{registrationId}+`.
2021-12-13 16:57:36 -06:00
By default, it is mapped to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
2019-09-28 12:19:03 -07:00
2020-08-28 12:42:44 -06:00
[[servlet-saml2login-rpr-uripatterns]]
2021-10-29 11:29:35 -06:00
=== URI Patterns
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
You probably noticed the `+{baseUrl}+` and `+{registrationId}+` placeholders in the preceding examples.
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
These are useful for generating URIs. As a result, the relying party's `entityId` and `assertionConsumerServiceLocation` support the following placeholders:
2020-04-06 14:36:33 -06:00
* `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
2021-12-13 16:57:36 -06:00
For example, the `assertionConsumerServiceLocation` defined earlier was:
2020-04-06 14:36:33 -06:00
`+/my-login-endpoint/{registrationId}+`
2021-12-13 16:57:36 -06:00
In a deployed application, it translates to:
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
`+/my-login-endpoint/adfs+`
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
The `entityId` shown earlier was defined as:
2020-04-06 14:36:33 -06:00
`+{baseUrl}/{registrationId}+`
2021-12-13 16:57:36 -06:00
In a deployed application, that translates to:
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
`+https://rp.example.com/adfs+`
2020-04-06 14:36:33 -06:00
2020-08-28 12:42:44 -06:00
[[servlet-saml2login-rpr-credentials]]
2021-10-29 11:29:35 -06:00
=== Credentials
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
In the example shown <<servlet-saml2login-relyingpartyregistration,earlier>>, you also likely noticed the credential that was used.
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
Oftentimes, a relying party uses the same key to sign payloads as well as decrypt them.
Alternatively, it can use the same key to verify payloads as well as encrypt them.
2020-04-06 14:36:33 -06:00
Because of this, Spring Security ships with `Saml2X509Credential`, a SAML-specific credential that simplifies configuring the same key for different use cases.
2021-12-13 16:57:36 -06:00
At a minimum, you need to have a certificate from the asserting party so that the asserting party's signed responses can be verified.
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
To construct a `Saml2X509Credential` that you can use to verify assertions from the asserting party, you can load the file and use
the `CertificateFactory`:
2020-04-06 14:36:33 -06:00
2021-06-16 10:31:29 +02:00
====
.Java
[source,java,role="primary"]
2020-04-06 14:36:33 -06:00
----
Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
2021-06-28 12:58:57 -06:00
X509Certificate certificate = (X509Certificate)
2020-04-06 14:36:33 -06:00
CertificateFactory.getInstance("X.509").generateCertificate(is);
2021-06-28 12:58:57 -06:00
return Saml2X509Credential.verification(certificate);
2020-04-06 14:36:33 -06:00
}
----
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
val resource = ClassPathResource("ap.crt")
resource.inputStream.use {
return Saml2X509Credential.verification(
CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
)
}
----
====
2021-12-13 16:57:36 -06:00
Suppose that the asserting party is going to also encrypt the assertion.
In that case, the relying party needs a private key to decrypt the encrypted value.
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
In that case, you need an `RSAPrivateKey` as well as its corresponding `X509Certificate`.
You can load the first by using Spring Security's `RsaKeyConverters` utility class and the second as you did before:
2020-04-06 14:36:33 -06:00
2021-06-16 10:31:29 +02:00
====
.Java
[source,java,role="primary"]
2020-04-06 14:36:33 -06:00
----
X509Certificate certificate = relyingPartyDecryptionCertificate();
Resource resource = new ClassPathResource("rp.crt");
try (InputStream is = resource.getInputStream()) {
2021-06-28 12:58:57 -06:00
RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
return Saml2X509Credential.decryption(rsa, certificate);
2020-04-06 14:36:33 -06:00
}
----
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
val certificate: X509Certificate = relyingPartyDecryptionCertificate()
val resource = ClassPathResource("rp.crt")
resource.inputStream.use {
val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
return Saml2X509Credential.decryption(rsa, certificate)
}
----
====
2020-04-06 14:36:33 -06:00
[TIP]
2021-12-13 16:57:36 -06:00
====
When you specify the locations of these files as the appropriate Spring Boot properties, Spring Boot performs these conversions for you.
====
2019-09-28 12:19:03 -07:00
2020-08-28 12:42:44 -06:00
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
2021-10-29 11:29:35 -06:00
=== Resolving the Relying Party from the Request
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration ID in the URI path.
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
You may want to customize for a number of reasons, including:
2020-08-28 12:42:44 -06:00
2021-12-13 16:57:36 -06:00
* You may know that your application is never going to be a multi-tenant application and, as a result, want a simpler URL scheme.
* You may identify tenants in a way other than by the URI path.
2020-08-28 12:42:44 -06:00
2021-03-02 07:55:05 -07:00
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
2021-12-13 16:57:36 -06:00
The default looks up the registration ID from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
2020-08-28 12:42:44 -06:00
You can provide a simpler resolver that, for example, always returns the same relying party:
2021-06-16 10:31:29 +02:00
====
.Java
[source,java,role="primary"]
2020-08-28 12:42:44 -06:00
----
2021-03-02 07:55:05 -07:00
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final RelyingPartyRegistrationResolver delegate;
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
}
2020-08-28 12:42:44 -06:00
2021-06-28 12:58:57 -06:00
@Override
2021-03-02 07:55:05 -07:00
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
return this.delegate.resolve(request, "single");
2020-08-28 12:42:44 -06:00
}
}
----
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
2021-03-02 07:55:05 -07:00
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
return this.delegate.resolve(request, "single")
2021-06-16 10:31:29 +02:00
}
}
----
====
2021-12-13 16:57:36 -06:00
Then you can provide this resolver to the appropriate filters that xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[produce `<saml2:AuthnRequest>` instances], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate `<saml2:Response>` instances], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
2020-08-28 12:42:44 -06:00
[NOTE]
2021-12-13 16:57:36 -06:00
====
2020-08-28 12:42:44 -06:00
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
2021-12-13 16:57:36 -06:00
====
2020-08-28 12:42:44 -06:00
[[servlet-saml2login-rpr-duplicated]]
2021-10-29 11:29:35 -06:00
=== Duplicated Relying Party Configurations
2020-04-06 14:36:33 -06:00
When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances:
2020-08-28 12:42:44 -06:00
* The relying party's `entityId`
2021-12-13 16:57:36 -06:00
* Its `assertionConsumerServiceLocation`
* Its credentials -- for example, its signing or decryption credentials
2019-09-28 12:19:03 -07:00
2021-12-13 16:57:36 -06:00
This setup may let credentials be more easily rotated for some identity providers versus others.
2019-09-28 12:19:03 -07:00
2020-04-06 14:36:33 -06:00
The duplication can be alleviated in a few different ways.
2019-09-28 12:19:03 -07:00
2021-12-13 16:57:36 -06:00
First, in YAML this can be alleviated with references:
2020-04-06 14:36:33 -06:00
2021-12-13 16:57:36 -06:00
====
2020-04-06 14:36:33 -06:00
[source,yaml]
----
spring:
security:
saml2:
relyingparty:
okta:
signing.credentials: &relying-party-credentials
- private-key-location: classpath:rp.key
2021-03-25 10:44:26 -06:00
certificate-location: classpath:rp.crt
2020-04-06 14:36:33 -06:00
identityprovider:
entity-id: ...
azure:
signing.credentials: *relying-party-credentials
identityprovider:
entity-id: ...
----
2021-12-13 16:57:36 -06:00
====
2019-09-28 12:19:03 -07:00
2021-12-13 16:57:36 -06:00
Second, in a database, you need not replicate the model of `RelyingPartyRegistration`.
2019-09-28 12:19:03 -07:00
2021-12-13 16:57:36 -06:00
Third, in Java, you can create a custom configuration method:
2019-09-28 12:19:03 -07:00
2021-06-16 10:31:29 +02:00
====
.Java
[source,java,role="primary"]
2020-04-06 14:36:33 -06:00
----
private RelyingPartyRegistration.Builder
addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {
2021-06-28 12:58:57 -06:00
Saml2X509Credential signingCredential = ...
builder.signingX509Credentials(c -> c.addAll(signingCredential));
// ... other relying party configurations
2020-04-06 14:36:33 -06:00
}
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
RelyingPartyRegistration okta = addRelyingPartyDetails(
2021-06-08 15:28:40 +03:00
RelyingPartyRegistrations
2020-04-06 14:36:33 -06:00
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("okta")).build();
RelyingPartyRegistration azure = addRelyingPartyDetails(
2021-06-08 15:28:40 +03:00
RelyingPartyRegistrations
2020-04-06 14:36:33 -06:00
.fromMetadataLocation(oktaMetadataUrl)
.registrationId("azure")).build();
return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
}
----
2019-09-28 12:19:03 -07:00
2021-06-16 10:31:29 +02:00
.Kotlin
[source,kotlin,role="secondary"]
----
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)
}
----
====