Restructure SAML 2.0 Documentation
- Add images - Standardize terminology - Add detail about working with OpenSAML - Reorganize sections Closes gh-8763
This commit is contained in:
parent
a9fe2cb377
commit
0a4766f21e
|
@ -40,7 +40,7 @@ Here's what you'll see in this release:
|
|||
** Added https://github.com/spring-projects/spring-security/issues/5185[XML support]
|
||||
** Improved https://github.com/spring-projects/spring-security/pull/7826[bearer token error handling] for JWT and Opaque Token
|
||||
* SAML 2.0
|
||||
** Added <<servlet-saml2-opensamlauthenticationprovider-authenticationmanager,AuthenticationManager>> configuration
|
||||
** Added <<servlet-saml2login-opensamlauthenticationprovider-authenticationmanager,AuthenticationManager>> configuration
|
||||
** Added support for https://github.com/spring-projects/spring-security/issues/7711[AuthNRequest signatures]
|
||||
** Added support for https://github.com/spring-projects/spring-security/pull/7759[AuthNRequest POST binding]
|
||||
|
||||
|
|
|
@ -3,32 +3,83 @@
|
|||
:figures: images/servlet/saml2
|
||||
:icondir: images/icons
|
||||
|
||||
The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Service Provider, having users log in to the application by using their existing account at a SAML 2.0 Identity Provider (Okta, ADFS, etc).
|
||||
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
|
||||
https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles].
|
||||
|
||||
[[servlet-saml2-spring-security-history]]
|
||||
Since 2009, support for service providers has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project].
|
||||
[[servlet-saml2login-spring-security-history]]
|
||||
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.
|
||||
This process is similar to the once started in 2017 for <<oauth2,Spring Security's OAuth 2.0 support>>.
|
||||
This process is similar to the one started in 2017 for <<oauth2,Spring Security's OAuth 2.0 support>>.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A working sample for {gh-samples-url}/boot/saml2login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security repository].
|
||||
====
|
||||
|
||||
Let's take a look at 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[]
|
||||
|
||||
The figure above builds off our <<servlet-securityfilterchain,`SecurityFilterChain`>> and <<servlet-authentication-abstractprocessingfilter, `AbstractAuthenticationProcessingFilter`>> diagrams:
|
||||
|
||||
image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized.
|
||||
|
||||
image:{icondir}/number_2.png[] Spring Security's <<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 <<servlet-exceptiontranslationfilter,`ExceptionTranslationFilter`>> initiates __Start Authentication__.
|
||||
The configured <<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.
|
||||
|
||||
[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
|
||||
.Authenticating a `<saml2:Response>`
|
||||
image::{figures}/saml2webssoauthenticationfilter.png[]
|
||||
|
||||
The figure builds off our <<servlet-securityfilterchain,`SecurityFilterChain`>> diagram.
|
||||
|
||||
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 <<servlet-authentication-providermanager,`AuthenticationManager`>>.
|
||||
By default, it will use the <<servlet-saml2login-architecture,`OpenSamlAuthenticationProvider`>>.
|
||||
|
||||
image:{icondir}/number_3.png[] If authentication fails, then __Failure__
|
||||
|
||||
* The <<servlet-authentication-securitycontextholder, `SecurityContextHolder`>> is cleared out.
|
||||
* The <<servlet-authentication-authenticationentrypoint,`AuthenticationEntryPoint`>> is invoked to restart the authentication process.
|
||||
|
||||
image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
|
||||
|
||||
* The <<servlet-authentication-authentication, `Authentication`>> is set on the <<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 is found in `spring-security-saml2-service-provider`.
|
||||
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.
|
||||
First, include the needed dependencies and second, indicate the necessary identity provider metadata.
|
||||
First, include the needed dependencies and second, indicate the necessary asserting party metadata.
|
||||
|
||||
[NOTE]
|
||||
Also, this presupposes that you've already <<servlet-saml2login-metadata, registered the relying party with your asserting party>>.
|
||||
|
||||
==== Specifying Identity Provider Metadata
|
||||
|
||||
|
@ -41,12 +92,12 @@ spring:
|
|||
saml2:
|
||||
relyingparty:
|
||||
registration:
|
||||
example:
|
||||
adfs:
|
||||
identityprovider:
|
||||
entity-id: https://idp.example.com/issuer
|
||||
verification.credentials:
|
||||
- certificate-location: "classpath:idp.crt"
|
||||
singlesignon.url: https://idp.example.com/issuer/SSO.saml2
|
||||
singlesignon.url: https://idp.example.com/issuer/sso
|
||||
singlesignon.sign-request: false
|
||||
----
|
||||
|
||||
|
@ -54,45 +105,132 @@ where
|
|||
|
||||
* `https://idp.example.com/issuer` is the value contained in the `Issuer` attribute of the SAML responses that the identity provider will issue
|
||||
* `classpath:idp.crt` is the location on the classpath for the identity provider's certificate for verifying SAML responses, and
|
||||
* `https://idp.example.com/issuer/SSO.saml2` is the endpoint where the identity provider is expecting `AuthnRequest` s.
|
||||
* `https://idp.example.com/issuer/sso` is the endpoint where the identity provider is expecting `AuthnRequest` s.
|
||||
|
||||
And that's it!
|
||||
|
||||
From here, consider jumping to:
|
||||
|
||||
* <<servlet-saml2login-architecture,How SAML 2.0 Login Works>>
|
||||
* <<servlet-saml2login-authenticatedprincipal,How to Use the `Saml2AuthenticatedPrincipal`>>
|
||||
* <<servlet-saml2login-sansboot,How to Configure without Spring Boot>>
|
||||
|
||||
[[servlet-saml2login-architecture]]
|
||||
=== How SAML 2.0 Login Works
|
||||
|
||||
When the above configuration is used, the application will automatically configure itself as a SAML 2.0 Service Provider - also called a relying party - that points to one or many identity providers - also called asserting parties.
|
||||
|
||||
[NOTE]
|
||||
Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party.
|
||||
These are frequently abbreviated as AP and RP, respectively.
|
||||
|
||||
There are two supported authentication flows:
|
||||
==== Runtime Expectations
|
||||
|
||||
1. AP-Initiated flow, which is when you login in directly to the asserting party, and then select a web application to be authenticated for.
|
||||
2. RP-Initiated flow, which is when you access your relying party, and it sends an authentication request to the asserting party.
|
||||
You then authenticate and are redirected back to the relying party.
|
||||
As configured above, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
|
||||
|
||||
To see this in action, you can navigate to a protected page in your app, for example `http://localhost:8080` is protected by default, and it will redirect you to the configured asserting party where you can authenticate.
|
||||
[source,html]
|
||||
----
|
||||
POST /login/saml2/sso/adfs HTTP/1.1
|
||||
|
||||
Once authenticated, the asserting party will issue an authentication response.
|
||||
Your application will then:
|
||||
SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
|
||||
----
|
||||
|
||||
1. Validate the response's signature against the set of public keys obtained from configuration
|
||||
2. Decrypt any encrypted assertions
|
||||
3. Validate the `ExpiresAt` and `NotBefore` timestamps, the `Issuer` url, the `<Subject>` and any `<AudienceRestriction>` conditions
|
||||
4. Map each attribute in the `AttributeStatement` into principal attributes
|
||||
5. Grant the authority `ROLE_USER` to the resulting authentication
|
||||
There are two ways to see induce your asserting party to generate a `SAMLResponse`:
|
||||
|
||||
[TIP]
|
||||
Because Spring Security iterates through the set of configured public keys, it's possible to achieve key rotation by adding a new key to the list before removing a key you are retiring.
|
||||
* 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`.
|
||||
|
||||
The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Saml2AuthenticatedPrincipal` object, and `Authentication#getName` maps to the first assertion's `NameID` element, if one is present.
|
||||
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 `OpenSamlAuthenticationRequestFactory` and `OpenSamlAuthenticationProvider` 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 `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 <<servlet-authentication-providermanager,`AuthenticationManager`>>.
|
||||
|
||||
image:{icondir}/number_2.png[] The <<servlet-authentication-providermanager,`AuthenticationManager`>> invokes the `OpenSamlAuthenticationProvider`.
|
||||
|
||||
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[] 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_5.png[] Then, the provider decrypts any encrypted assertions.
|
||||
If any decryptions fail, 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.
|
||||
It is required that either the response or the assertions have signatures.
|
||||
|
||||
image:{icondir}/number_7.png[] Then, 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_8.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_9.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:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
static {
|
||||
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, in the event that you always want to always force the asserting party to reauthenticate the user, you can register your own `AuthnRequestMarshaller`, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
static {
|
||||
OpenSamlInitializationService.requireInitialize(factory -> {
|
||||
AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
|
||||
@Override
|
||||
public Element marshall(XMLObject object, Element element) throws MarshallingException {
|
||||
configureAuthnRequest((AuthnRequest) object);
|
||||
return super.marshall(object, element);
|
||||
}
|
||||
|
||||
public Element marshall(XMLObject object, Document document) throws MarshallingException {
|
||||
configureAuthnRequest((AuthnRequest) object);
|
||||
return super.marshall(object, element);
|
||||
}
|
||||
|
||||
private void configureAuthnRequest(AuthnRequest authnRequest) {
|
||||
authnRequest.setForceAuthN(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
----
|
||||
|
||||
The `requireInitialize` method may only be called once per application instance.
|
||||
|
||||
[[servlet-saml2login-sansboot]]
|
||||
=== Overriding or Replacing Boot Auto Configuration
|
||||
|
@ -132,7 +270,7 @@ fun configure(http: HttpSecurity) {
|
|||
|
||||
If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one.
|
||||
|
||||
Replacing this is as simple as exposing the bean within the application:
|
||||
You can replace this by exposing the bean within the application:
|
||||
|
||||
.Custom SAML 2.0 Login Configuration
|
||||
====
|
||||
|
@ -174,7 +312,7 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
|
|||
The above requires the role of `USER` for any URL that starts with `/messages/`.
|
||||
|
||||
[[servlet-saml2login-relyingpartyregistrationrepository]]
|
||||
The second `@Bean` Spring Boot creates is a `RelyingPartyRegistrationRepository`, which represents the AP and RP metadata.
|
||||
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.
|
||||
|
@ -230,7 +368,7 @@ Note that `X509Support` is an OpenSAML class, used here in the snippet for brevi
|
|||
|
||||
[[servlet-saml2login-relyingpartyregistrationrepository-dsl]]
|
||||
|
||||
Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configuration:
|
||||
Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configured `WebSecurityConfigurerAdapter`:
|
||||
|
||||
.Custom Relying Party Registration DSL
|
||||
====
|
||||
|
@ -275,14 +413,14 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
|
|||
[NOTE]
|
||||
A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`.
|
||||
|
||||
[[servlet-saml2-relyingpartyregistration]]
|
||||
[[servlet-saml2login-relyingpartyregistration]]
|
||||
=== RelyingPartyRegistration
|
||||
A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
|
||||
instance represents a link between an RP and AP's metadata.
|
||||
instance represents a link between an relying party and assering party's metadata.
|
||||
|
||||
In a `RelyingPartyRegistration`, you can provide RP 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.
|
||||
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 AP metadata like its `Issuer` value, where it expects AuthnRequests to be sent to, and any credentials that it owns for the purposes of the RP verifying or encrypting paylods.
|
||||
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:
|
||||
|
||||
|
@ -310,23 +448,24 @@ RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.wit
|
|||
----
|
||||
|
||||
[TIP]
|
||||
The top-level metadata methods are details about the RP. The methods inside `assertingPartyDetails` are details about the AP.
|
||||
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 an RP is expecting SAML Responses is known as the Assertion Consumer Service Location.
|
||||
The location where a relying party is expecting SAML Responses is the Assertion Consumer Service Location.
|
||||
|
||||
The default for the RP's `entityId` is `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`.
|
||||
This is this value needed when configuring the AP to know about your RP.
|
||||
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 {security-api-url}org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.html[`Saml2WebSsoAuthenticationFilter`] in the filter chain.
|
||||
It's mapped by default to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
|
||||
|
||||
[[servlet-saml2-rpr-uripatterns]]
|
||||
[[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 RP's `entityId` and `assertionConsumerServiceLocation` support the following 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
|
||||
|
@ -340,7 +479,7 @@ For example, the `assertionConsumerServiceLocation` defined above was:
|
|||
|
||||
which in a deployed application would translate to
|
||||
|
||||
`+/my-login-endpoint/my-id+`
|
||||
`+/my-login-endpoint/adfs+`
|
||||
|
||||
The `entityId` above was defined as:
|
||||
|
||||
|
@ -348,21 +487,22 @@ The `entityId` above was defined as:
|
|||
|
||||
which in a deployed application would translate to
|
||||
|
||||
`+https://rp.example.org/my-id+`
|
||||
`+https://rp.example.com/adfs+`
|
||||
|
||||
[[servlet-saml2-rpr-credentials]]
|
||||
[[servlet-saml2login-rpr-credentials]]
|
||||
==== Credentials
|
||||
|
||||
You also likely noticed the credential that was used.
|
||||
|
||||
Oftentimes, an RP will use the same key to sign payloads as well as decrypt them.
|
||||
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 AP's signed responses can be verified.
|
||||
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 `CertificateFactory` like so:
|
||||
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:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
@ -374,8 +514,8 @@ try (InputStream is = resource.getInputStream()) {
|
|||
}
|
||||
----
|
||||
|
||||
Let's say that the Asserting Party is going to also encrypt the assertion.
|
||||
In that case, the Relying Party will need a private key to be able to decrypt the encrypted value.
|
||||
Let's say that the asserting party is going to also encrypt the assertion.
|
||||
In that case, the relying party will need a private key to be able to decrypt the encrypted value.
|
||||
|
||||
In that case, you'll need an `RSAPrivateKey` as well as its corresponding `X509Certificate`.
|
||||
You can load the first using Spring Security's `RsaKeyConverters` utility class and the second as you did before:
|
||||
|
@ -393,12 +533,44 @@ try (InputStream is = resource.getInputStream()) {
|
|||
[TIP]
|
||||
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
|
||||
|
||||
[[servlet-saml2-rpr-duplicated]]
|
||||
[[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
|
||||
==== Resolving the Relying Party from the Request
|
||||
|
||||
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
|
||||
|
||||
There are a number of reasons you may want to customize. Among them:
|
||||
|
||||
* You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme
|
||||
* You may identify tenants in a way other than by the URI path
|
||||
|
||||
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `Converter<HttpServletRequest, RelyingPartyRegistration>`.
|
||||
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
|
||||
|
||||
You can provide a simpler resolver that, for example, always returns the same relying party:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class SingleRelyingPartyRegistrationResolver
|
||||
implements Converter<HttpServletRequest, RelyingPartyRegistration> {
|
||||
|
||||
@Override
|
||||
public RelyingPartyRegistration convert(HttpServletRequest request) {
|
||||
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 RP's `entityId`
|
||||
* The relying party's `entityId`
|
||||
* Its `assertionConsumerServiceLocation`, and
|
||||
* Its credentials, for example its signing or decryption credentials
|
||||
|
||||
|
@ -426,7 +598,7 @@ spring:
|
|||
entity-id: ...
|
||||
----
|
||||
|
||||
Second, in a database, it's not necessary to replicate `RelyingPartyRegistration`'s model.
|
||||
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:
|
||||
|
||||
|
@ -456,27 +628,65 @@ public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
|
|||
}
|
||||
----
|
||||
|
||||
[[servlet-saml2-sp-initiated]]
|
||||
=== RP-initiated Login
|
||||
[[servlet-saml2login-sp-initiated-factory]]
|
||||
=== Producing `<saml2:AuthnRequest>` s
|
||||
|
||||
You can initiate login by navigating to the SAML 2.0 login endpoint for a given `RelyingPartyRegistration`, like so:
|
||||
As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party.
|
||||
|
||||
`+{baseUrl}/saml2/authenticate/{registrationId}+`
|
||||
Spring Security achieves this in part by registering the `Saml2WebSsoAuthenticationRequestFilter` in the filter chain.
|
||||
This filter by default responds to endpoint `+/saml2/authenticate/{registrationId}+`.
|
||||
|
||||
Where you replace `+{baseUrl}+` and `+{registrationId}+` with the deployed location of your application and the identifier for that registration.
|
||||
|
||||
For example, if you were deployed to `https://rp.example.org` and you gave your registration an ID of `ping`, you could navigate to:
|
||||
For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to:
|
||||
|
||||
`https://rp.example.org/saml2/authenticate/ping`
|
||||
|
||||
You can modify this by post-processing the `Saml2WebSsoAuthenticationRequestFilter`.
|
||||
and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
|
||||
|
||||
By default, this filter will use HTTP-Redirect for the AuthNRequest.
|
||||
However, by setting `RelyingPartyRegistration.AssertingPartyDetails#singleSignOnServiceBinding` to `Saml2MessageType.POST`, like so, it will use HTTP-POST instead:
|
||||
[[servlet-saml2login-sp-initiated-factory-signing]]
|
||||
==== Changing How the `<saml2:AuthnRequest>` Gets Sent
|
||||
|
||||
By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party.
|
||||
|
||||
Many asserting parties don't require a signed `<saml2:AuthnRequest>`.
|
||||
This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
|
||||
|
||||
|
||||
.Not Requiring Signed AuthnRequests
|
||||
====
|
||||
.Boot
|
||||
[source,yaml,role="primary"]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
saml2:
|
||||
relyingparty:
|
||||
okta:
|
||||
identityprovider:
|
||||
entity-id: ...
|
||||
singlesignon.sign-request: false
|
||||
----
|
||||
|
||||
.Java
|
||||
[source,java,role="secondary"]
|
||||
----
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
|
||||
// ...
|
||||
.assertingPartyDetails(party -> party
|
||||
// ...
|
||||
.wantAuthnRequestsSigned(false)
|
||||
);
|
||||
----
|
||||
====
|
||||
|
||||
Otherwise, you will need to specify a private key to `RelyingPartyRegistration#signingX509Credentials` so that Spring Security can sign the `<saml2:AuthnRequest>` before sending.
|
||||
|
||||
[[servlet-saml2login-sp-initiated-factory-binding]]
|
||||
Some asserting parties require that the `<saml2:AuthnRequest>` be POSTed.
|
||||
This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
|
||||
// ...
|
||||
.assertingPartyDetails(party -> party
|
||||
// ...
|
||||
|
@ -484,16 +694,16 @@ RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.wit
|
|||
);
|
||||
----
|
||||
|
||||
[[servlet-saml2-sp-initiated-factory]]
|
||||
==== Customizing the AuthNRequest
|
||||
[[servlet-saml2login-sp-initiated-factory-custom-authnrequest]]
|
||||
==== Customizing OpenSAML's `AuthnRequest` Instance
|
||||
|
||||
There are a number of reasons that you may want to adjust an AuthNRequest.
|
||||
There are a number of reasons that you may want to adjust an `AuthnRequest`.
|
||||
For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
|
||||
|
||||
If you don't need information from the `HttpServletRequest` to make your decision, then the easiest way is to register a custom `AuthnRequestMarshaller` with OpenSAML.
|
||||
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 it yourself, like so:
|
||||
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:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
@ -507,24 +717,24 @@ public class AuthnRequestConverter implements
|
|||
// ... constructor
|
||||
|
||||
public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
|
||||
MySaml2AuthenticationRequestContext custom = (MySaml2AuthenticationRequestContext) context;
|
||||
MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context;
|
||||
Issuer issuer = issuerBuilder.buildObject();
|
||||
issuer.setValue(context.getIssuer());
|
||||
issuer.setValue(myContext.getIssuer());
|
||||
|
||||
AuthnRequest authnRequest = authnRequestBuilder.buildObject();
|
||||
authnRequest.setIssuer(iss);
|
||||
authnRequest.setDestination(context.getDestination());
|
||||
authnRequest.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl());
|
||||
authnRequest.setDestination(myContext.getDestination());
|
||||
authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl());
|
||||
|
||||
// ... additional settings
|
||||
|
||||
authRequest.setForceAuthn(custom.getForceAuthn());
|
||||
authRequest.setForceAuthn(myContext.getForceAuthn());
|
||||
return authnRequest;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
And then you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory`, and publish them as `@Bean` s:
|
||||
Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as `@Bean` s:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
@ -549,10 +759,10 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory(
|
|||
}
|
||||
----
|
||||
|
||||
[[servlet-saml2-login-customize]]
|
||||
=== Customizing Authentication Logic
|
||||
[[servlet-saml2login-authenticate-responses]]
|
||||
=== Authenticating `<saml2:Response>` s
|
||||
|
||||
To verify SAML 2.0 Responses, Spring Security uses `OpenSamlAuthenticationProvider` by default.
|
||||
To verify SAML 2.0 Responses, Spring Security uses <<servlet-saml2login-architecture,`OpenSamlAuthenticationProvider`>> by default.
|
||||
|
||||
You can configure this in a number of ways including:
|
||||
|
||||
|
@ -562,7 +772,7 @@ You can configure this in a number of ways including:
|
|||
|
||||
To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
|
||||
|
||||
[[servlet-saml2-opensamlauthenticationprovider-authenticationmanager]]
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
|
||||
==== Setting a Clock Skew
|
||||
|
||||
It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized.
|
||||
|
@ -596,6 +806,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
}
|
||||
----
|
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
|
||||
==== Coordinating with a `UserDetailsService`
|
||||
|
||||
Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
|
||||
|
@ -632,14 +843,15 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
}
|
||||
----
|
||||
<1> First, call the default converter, which extracts attributes and authorities from the response
|
||||
<2> Second, call the `UserDetailsService` using the relevant information
|
||||
<2> Second, call the <<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 `OpenSamlAuthenticationProvider` 's default authentication converter.
|
||||
It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from `AttributeStatement` s as well as the single `ROLE_USER` authority.
|
||||
|
||||
==== Performing additional validation
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
|
||||
==== Performing Additional Validation
|
||||
|
||||
`OpenSamlAuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
|
||||
After verifying the signature, it will:
|
||||
|
@ -649,6 +861,7 @@ After verifying the signature, it will:
|
|||
|
||||
To perform additional validation, you can configure your own assertion validator that delegates to `OpenSamlAuthenticationProvider` 's default and then performs its own.
|
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]]
|
||||
For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
|
||||
|
||||
[source,java]
|
||||
|
@ -677,11 +890,12 @@ provider.setAssertionValidator(assertionToken -> {
|
|||
While recommended, it's not necessary to call `OpenSamlAuthenticationProvider` '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.
|
||||
|
||||
[[servlet-saml2-custom-authenticationmanager]]
|
||||
[[servlet-saml2login-authenticationmanager-custom]]
|
||||
==== Using a Custom Authentication Manager
|
||||
|
||||
Of course, the `authenticationManager` DSL method can be used to perform a completely custom SAML 2.0 authentication.
|
||||
This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2 Response XML data.
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]]
|
||||
Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication.
|
||||
This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2.0 Response XML data.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
@ -729,7 +943,7 @@ Because the SAML 2.0 specification allows for each attribute to have multiple va
|
|||
`getFirstAttribute` is quite handy when you know that there is only one value.
|
||||
|
||||
[[servlet-saml2login-metadata]]
|
||||
=== Publishing a Metadata Endpoint
|
||||
=== Producing `<saml2:SPSSODescriptor>` Metadata
|
||||
|
||||
You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
|
||||
|
||||
|
@ -747,6 +961,9 @@ http
|
|||
.addFilterBefore(new Saml2MetadataFilter(r), Saml2WebSsoAuthenticationFilter.class);
|
||||
----
|
||||
|
||||
You can use this metadata endpoint to register your relying party with your asserting party.
|
||||
This is often as simple as finding the correct form field to supply the metadata endpoint.
|
||||
|
||||
By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`.
|
||||
You can change this by calling the `setRequestMatcher` method on the filter:
|
||||
|
||||
|
@ -754,3 +971,24 @@ You can change this by calling the `setRequestMatcher` method on the filter:
|
|||
----
|
||||
filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
|
||||
----
|
||||
|
||||
[[servlet-saml2login-logout]]
|
||||
=== Performing Single Logout
|
||||
|
||||
Spring Security does not yet support single logout.
|
||||
|
||||
Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
http
|
||||
// ...
|
||||
.logout(logout -> logout
|
||||
.logoutSuccessHandler(myCustomSuccessHandler())
|
||||
.logoutRequestMatcher(myRequestMatcher())
|
||||
)
|
||||
----
|
||||
|
||||
The success handler will send logout requests to the asserting party.
|
||||
|
||||
The request matcher will detect logout requests from the asserting party.
|
||||
|
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 152 KiB |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
Loading…
Reference in New Issue