mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-31 09:12:14 +00:00
916 lines
38 KiB
Plaintext
916 lines
38 KiB
Plaintext
= SAML 2.0 Login Overview
|
|
:figures: servlet/saml2
|
|
:icondir: icons
|
|
|
|
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:
|
|
|
|
.Redirecting to Asserting Party Authentication
|
|
image::{figures}/saml2webssoauthenticationrequestfilter.png[]
|
|
|
|
[NOTE]
|
|
====
|
|
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_1.png[] First, a user makes an unauthenticated request to the `/private` resource, for which it is not authorized.
|
|
|
|
image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] 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`.
|
|
Alternatively, if you have <<servlet-saml2login-relyingpartyregistrationrepository,configured more than one asserting party>>, it first redirects 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 tries to authentication the user.
|
|
If successful, it returns 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.
|
|
|
|
The following image shows how Spring Security authenticates a `<saml2:Response>`.
|
|
|
|
[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
|
|
.Authenticating a `<saml2:Response>`
|
|
image::{figures}/saml2webssoauthenticationfilter.png[]
|
|
|
|
[NOTE]
|
|
====
|
|
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
|
|
====
|
|
|
|
[[servlet-saml2login-authentication-saml2authenticationtokenconverter]]
|
|
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`].
|
|
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`].
|
|
By default, it uses the <<servlet-saml2login-architecture,`OpenSamlAuthenticationProvider`>>.
|
|
|
|
image:{icondir}/number_3.png[] If authentication fails, then _Failure_.
|
|
|
|
* 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.
|
|
|
|
image:{icondir}/number_4.png[] If authentication is successful, then _Success_.
|
|
|
|
* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`].
|
|
* The `Saml2WebSsoAuthenticationFilter` invokes `FilterChain#doFilter(request,response)` to continue with the rest of the application logic.
|
|
|
|
[[servlet-saml2login-minimaldependencies]]
|
|
== Minimal Dependencies
|
|
|
|
SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`.
|
|
It builds off of the OpenSAML library.
|
|
|
|
[[servlet-saml2login-minimalconfiguration]]
|
|
== Minimal Configuration
|
|
|
|
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.
|
|
|
|
[NOTE]
|
|
Also, this configuration presupposes that you have already xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[registered the relying party with your asserting party].
|
|
|
|
[[saml2-specifying-identity-provider-metadata]]
|
|
=== Specifying Identity Provider Metadata
|
|
|
|
In a Spring Boot application, to specify an identity provider's metadata, create configuration similar to the following:
|
|
|
|
====
|
|
[source,yml]
|
|
----
|
|
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 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.
|
|
* `adfs` is <<servlet-saml2login-relyingpartyregistrationid, an arbitrary identifier you choose>>
|
|
|
|
And that's it!
|
|
|
|
[NOTE]
|
|
====
|
|
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 <<saml2-specifying-identity-provider-metadata,earlier>>, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
|
|
|
|
====
|
|
[source,http]
|
|
----
|
|
POST /login/saml2/sso/adfs HTTP/1.1
|
|
|
|
SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
|
|
----
|
|
====
|
|
|
|
There are two ways to induce your asserting party to generate a `SAMLResponse`:
|
|
|
|
* 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`.
|
|
* 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`.
|
|
|
|
From here, consider jumping to:
|
|
|
|
* <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
|
|
* xref:servlet/saml2/login/authentication.adoc#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:
|
|
|
|
* Rely on a library for SAML 2.0 operations and domain objects.
|
|
To achieve this, Spring Security uses OpenSAML.
|
|
* Ensure that 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 an unsupported version of OpenSAML.
|
|
|
|
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` implementations that customize various steps in the authentication process.
|
|
|
|
For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter delegates to `OpenSamlAuthenticationProvider`:
|
|
|
|
.Authenticating an OpenSAML `Response`
|
|
image:{figures}/opensamlauthenticationprovider.png[]
|
|
|
|
This figure builds off of the <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter` diagram>>.
|
|
|
|
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 xref:servlet/saml2/login/authentication.adoc#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 do not 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 xref:servlet/saml2/login/authentication.adoc#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.
|
|
`Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId` holds the <<servlet-saml2login-relyingpartyregistrationid,identifier to the associated `RelyingPartyRegistration`>>.
|
|
|
|
[[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:
|
|
|
|
====
|
|
.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:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
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
|
|
[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)
|
|
}
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
The `requireInitialize` method may be called only once per application instance.
|
|
|
|
[[servlet-saml2login-sansboot]]
|
|
== Overriding or Replacing Boot Auto Configuration
|
|
|
|
Spring Boot generates two `@Bean` objects for a relying party.
|
|
|
|
The first is a `SecurityFilterChain` that configures the application as a relying party.
|
|
When including `spring-security-saml2-service-provider`, the `SecurityFilterChain` looks like:
|
|
|
|
.Default SAML 2.0 Login Configuration
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@Bean
|
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
http
|
|
.authorizeHttpRequests(authorize -> authorize
|
|
.anyRequest().authenticated()
|
|
)
|
|
.saml2Login(withDefaults());
|
|
return http.build();
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@Bean
|
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
http {
|
|
authorizeRequests {
|
|
authorize(anyRequest, authenticated)
|
|
}
|
|
saml2Login { }
|
|
}
|
|
return http.build()
|
|
}
|
|
----
|
|
====
|
|
|
|
If the application does not expose a `SecurityFilterChain` bean, Spring Boot exposes the preceding default one.
|
|
|
|
You can replace this by exposing the bean within the application:
|
|
|
|
.Custom SAML 2.0 Login Configuration
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSecurity
|
|
public class MyCustomSecurityConfiguration {
|
|
@Bean
|
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
http
|
|
.authorizeHttpRequests(authorize -> authorize
|
|
.requestMatchers("/messages/**").hasAuthority("ROLE_USER")
|
|
.anyRequest().authenticated()
|
|
)
|
|
.saml2Login(withDefaults());
|
|
return http.build();
|
|
}
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSecurity
|
|
class MyCustomSecurityConfiguration {
|
|
@Bean
|
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
http {
|
|
authorizeRequests {
|
|
authorize("/messages/**", hasAuthority("ROLE_USER"))
|
|
authorize(anyRequest, authenticated)
|
|
}
|
|
saml2Login {
|
|
}
|
|
}
|
|
return http.build()
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
The preceding example requires the role of `USER` for any URL that starts with `/messages/`.
|
|
|
|
[[servlet-saml2login-relyingpartyregistrationrepository]]
|
|
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 such things as 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:
|
|
|
|
.Relying Party Registration Repository
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@Value("${metadata.location}")
|
|
String assertingPartyMetadataLocation;
|
|
|
|
@Bean
|
|
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
|
|
RelyingPartyRegistration registration = RelyingPartyRegistrations
|
|
.fromMetadataLocation(assertingPartyMetadataLocation)
|
|
.registrationId("example")
|
|
.build();
|
|
return new InMemoryRelyingPartyRegistrationRepository(registration);
|
|
}
|
|
----
|
|
|
|
.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)
|
|
}
|
|
----
|
|
====
|
|
|
|
[[servlet-saml2login-relyingpartyregistrationid]]
|
|
[NOTE]
|
|
The `registrationId` is an arbitrary value that you choose for differentiating between registrations.
|
|
|
|
Alternatively, you can provide each detail manually:
|
|
|
|
.Relying Party Registration Repository Manual Configuration
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@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
|
|
[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)
|
|
}
|
|
----
|
|
====
|
|
|
|
[NOTE]
|
|
====
|
|
`X509Support` is an OpenSAML class, used in the preceding snippet for brevity.
|
|
====
|
|
|
|
[[servlet-saml2login-relyingpartyregistrationrepository-dsl]]
|
|
|
|
Alternatively, you can directly wire up the repository by using the DSL, which also overrides the auto-configured `SecurityFilterChain`:
|
|
|
|
.Custom Relying Party Registration DSL
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSecurity
|
|
public class MyCustomSecurityConfiguration {
|
|
@Bean
|
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
http
|
|
.authorizeHttpRequests(authorize -> authorize
|
|
.requestMatchers("/messages/**").hasAuthority("ROLE_USER")
|
|
.anyRequest().authenticated()
|
|
)
|
|
.saml2Login(saml2 -> saml2
|
|
.relyingPartyRegistrationRepository(relyingPartyRegistrations())
|
|
);
|
|
return http.build();
|
|
}
|
|
}
|
|
----
|
|
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSecurity
|
|
class MyCustomSecurityConfiguration {
|
|
@Bean
|
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
|
http {
|
|
authorizeRequests {
|
|
authorize("/messages/**", hasAuthority("ROLE_USER"))
|
|
authorize(anyRequest, authenticated)
|
|
}
|
|
saml2Login {
|
|
relyingPartyRegistrationRepository = relyingPartyRegistrations()
|
|
}
|
|
}
|
|
return http.build()
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
[NOTE]
|
|
====
|
|
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 an asserting 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
|
|
[source,java,role="primary"]
|
|
----
|
|
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
|
|
.fromMetadataLocation("https://ap.example.org/metadata")
|
|
.registrationId("my-id")
|
|
.build();
|
|
----
|
|
.Kotlin
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
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:
|
|
|
|
[source,java]
|
|
----
|
|
String xml = fromDatabase();
|
|
try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
|
|
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
|
|
.fromMetadata(source)
|
|
.registrationId("my-id")
|
|
.build();
|
|
}
|
|
----
|
|
|
|
A more sophisticated setup is also possible:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
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
|
|
[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()
|
|
----
|
|
====
|
|
|
|
[TIP]
|
|
====
|
|
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}+`.
|
|
By default, it is mapped to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
|
|
|
|
[[servlet-saml2login-rpr-uripatterns]]
|
|
=== URI Patterns
|
|
|
|
You probably noticed the `+{baseUrl}+` and `+{registrationId}+` placeholders in the preceding examples.
|
|
|
|
These are useful for generating URIs. As a result, 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 earlier was:
|
|
|
|
`+/my-login-endpoint/{registrationId}+`
|
|
|
|
In a deployed application, it translates to:
|
|
|
|
`+/my-login-endpoint/adfs+`
|
|
|
|
The `entityId` shown earlier was defined as:
|
|
|
|
`+{baseUrl}/{registrationId}+`
|
|
|
|
In a deployed application, that translates to:
|
|
|
|
`+https://rp.example.com/adfs+`
|
|
|
|
The prevailing URI patterns are as follows:
|
|
|
|
* `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a `<saml2:AuthnRequest>`] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party
|
|
* `+/login/saml2/sso/+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's `<saml2:Response>`]; the `RelyingPartyRegistration` is looked up from previously authenticated state or the response's issuer if needed; also supports `+/login/saml2/sso/{registrationId}+`
|
|
* `+/logout/saml2/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `<saml2:LogoutRequest>` and `<saml2:LogoutResponse>` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state or the request's issuer if needed; also supports `+/logout/saml2/slo/{registrationId}+`
|
|
* `+/saml2/metadata+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for the set of ``RelyingPartyRegistration``s; also supports `+/saml2/metadata/{registrationId}+` or `+/saml2/service-provider-metadata/{registrationId}+` for a specific `RelyingPartyRegistration`
|
|
|
|
Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios.
|
|
If you wish to remove the `registrationId` from the URL for any reason, you can <<servlet-saml2login-rpr-relyingpartyregistrationresolver,specify a `RelyingPartyRegistrationResolver`>> to tell Spring Security how to look up the `registrationId`.
|
|
|
|
[[servlet-saml2login-rpr-credentials]]
|
|
=== Credentials
|
|
|
|
In the example shown <<servlet-saml2login-relyingpartyregistration,earlier>>, you also likely noticed the credential that was used.
|
|
|
|
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.
|
|
|
|
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, you need 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 can use to verify assertions from the asserting party, you can load the file and use
|
|
the `CertificateFactory`:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
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
|
|
[source,kotlin,role="secondary"]
|
|
----
|
|
val resource = ClassPathResource("ap.crt")
|
|
resource.inputStream.use {
|
|
return Saml2X509Credential.verification(
|
|
CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
|
|
)
|
|
}
|
|
----
|
|
====
|
|
|
|
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.
|
|
|
|
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:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
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
|
|
[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)
|
|
}
|
|
----
|
|
====
|
|
|
|
[TIP]
|
|
====
|
|
When you specify the locations of these files as the appropriate Spring Boot properties, Spring Boot performs these conversions for you.
|
|
====
|
|
|
|
[[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`
|
|
* Its credentials -- for example, its signing or decryption credentials
|
|
|
|
This setup may let credentials be more easily rotated for some identity providers versus others.
|
|
|
|
The duplication can be alleviated in a few different ways.
|
|
|
|
First, in YAML this can be alleviated with references:
|
|
|
|
====
|
|
[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, you need not replicate the model of `RelyingPartyRegistration`.
|
|
|
|
Third, in Java, you can create a custom configuration method:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
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
|
|
[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)
|
|
}
|
|
----
|
|
====
|
|
|
|
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
|
|
=== Resolving the `RelyingPartyRegistration` from the Request
|
|
|
|
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
|
|
|
|
Depending on the use case, a number of other strategies are also employed to derive one.
|
|
For example:
|
|
|
|
* For processing `<saml2:Response>`s, the `RelyingPartyRegistration` is looked up from the associated `<saml2:AuthRequest>` or from the `<saml2:Response#Issuer>` element
|
|
* For processing `<saml2:LogoutRequest>`s, the `RelyingPartyRegistration` is looked up from the currently logged in user or from the `<saml2:LogoutRequest#Issuer>` element
|
|
* For publishing metadata, the `RelyingPartyRegistration`s are looked up from any repository that also implements `Iterable<RelyingPartyRegistration>`
|
|
|
|
When this needs adjustment, you can turn to the specific components for each of these endpoints targeted at customizing this:
|
|
|
|
* For SAML Responses, customize the `AuthenticationConverter`
|
|
* For Logout Requests, customize the `Saml2LogoutRequestValidatorParametersResolver`
|
|
* For Metadata, customize the `Saml2MetadataResponseResolver`
|
|
|
|
[[federating-saml2-login]]
|
|
=== Federating Login
|
|
|
|
One common arrangement with SAML 2.0 is an identity provider that has multiple asserting parties.
|
|
In this case, the identity provider's metadata endpoint returns multiple `<md:IDPSSODescriptor>` elements.
|
|
|
|
These multiple asserting parties can be accessed in a single call to `RelyingPartyRegistrations` like so:
|
|
|
|
====
|
|
.Java
|
|
[source,java,role="primary"]
|
|
----
|
|
Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
|
|
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
|
|
.stream().map((builder) -> builder
|
|
.registrationId(UUID.randomUUID().toString())
|
|
.entityId("https://example.org/saml2/sp")
|
|
.build()
|
|
)
|
|
.collect(Collectors.toList()));
|
|
----
|
|
|
|
.Kotlin
|
|
[source,java,role="secondary"]
|
|
----
|
|
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
|
|
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
|
|
.stream().map { builder : RelyingPartyRegistration.Builder -> builder
|
|
.registrationId(UUID.randomUUID().toString())
|
|
.entityId("https://example.org/saml2/sp")
|
|
.assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso")
|
|
.build()
|
|
}
|
|
.collect(Collectors.toList()));
|
|
----
|
|
====
|
|
|
|
Note that because the registration id is set to a random value, this will change certain SAML 2.0 endpoints to be unpredictable.
|
|
There are several ways to address this; let's focus on a way that suits the specific use case of federation.
|
|
|
|
In many federation cases, all the asserting parties share service provider configuration.
|
|
Given that Spring Security will by default include the `registrationId` in the service provider metadata, another step is to change corresponding URIs to exclude the `registrationId`, which you can see has already been done in the above sample where the `entityId` and `assertionConsumerServiceLocation` are configured with a static endpoint.
|
|
|
|
You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
|
|
|
|
[[using-spring-security-saml-extension-uris]]
|
|
=== Using Spring Security SAML Extension URIs
|
|
|
|
In the event that you are migrating from the Spring Security SAML Extension, there may be some benefit to configuring your application to use the SAML Extension URI defaults.
|
|
|
|
For more information on this, please see {gh-samples-url}/servlet/spring-boot/java/saml2/custom-urls[our `custom-urls` sample] and {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
|