|
@@ -1,4 +1,1618 @@
|
|
[[servlet-saml2]]
|
|
[[servlet-saml2]]
|
|
= SAML2
|
|
= SAML2
|
|
|
|
|
|
-include::saml2-login.adoc[]
|
|
|
|
|
|
+Spring Security provides comprehensive SAML 2 support.
|
|
|
|
+This section discusses how to integrate SAML 2 into your servlet based application.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login]]
|
|
|
|
+== SAML 2.0 Login
|
|
|
|
+: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 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-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 one started in 2017 for <<oauth2,Spring Security's OAuth 2.0 support>>.
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+====
|
|
|
|
+A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository].
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security.
|
|
|
|
+First, we see that, like <<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,`OpenSAML authentication provider`>>.
|
|
|
|
+
|
|
|
|
+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 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 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
|
|
|
|
+
|
|
|
|
+In a Spring Boot application, to specify an identity provider's metadata, simply do:
|
|
|
|
+
|
|
|
|
+[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 will issue
|
|
|
|
+* `classpath:idp.crt` is the location on the classpath for the identity provider's certificate for verifying SAML responses, and
|
|
|
|
+* `https://idp.example.com/issuer/sso` is the endpoint where the identity provider is expecting `AuthnRequest` s.
|
|
|
|
+
|
|
|
|
+And that's it!
|
|
|
|
+
|
|
|
|
+[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 above, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
|
|
|
|
+
|
|
|
|
+[source,html]
|
|
|
|
+----
|
|
|
|
+POST /login/saml2/sso/adfs HTTP/1.1
|
|
|
|
+
|
|
|
|
+SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+There are two ways to see induce your asserting party to generate a `SAMLResponse`:
|
|
|
|
+
|
|
|
|
+* First, you can navigate to your asserting party.
|
|
|
|
+It likely has some kind of link or button for each registered relying party that you can click to send the `SAMLResponse`.
|
|
|
|
+* Second, you can navigate to a protected page in your app, for example, `http://localhost:8080`.
|
|
|
|
+Your app then redirects to the configured asserting party which then sends the `SAMLResponse`.
|
|
|
|
+
|
|
|
|
+From here, consider jumping to:
|
|
|
|
+
|
|
|
|
+* <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
|
|
|
|
+* <<servlet-saml2login-authenticatedprincipal,How to Use the `Saml2AuthenticatedPrincipal`>>
|
|
|
|
+* <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-architecture]]
|
|
|
|
+=== How SAML 2.0 Login Integrates with OpenSAML
|
|
|
|
+
|
|
|
|
+Spring Security's SAML 2.0 support has a couple of design goals:
|
|
|
|
+
|
|
|
|
+* First, rely on a library for SAML 2.0 operations and domain objects.
|
|
|
|
+To achieve this, Spring Security uses OpenSAML.
|
|
|
|
+* Second, ensure this library is not required when using Spring Security's SAML support.
|
|
|
|
+To achieve this, any interfaces or classes where Spring Security uses OpenSAML in the contract remain encapsulated.
|
|
|
|
+This makes it possible for you to switch out OpenSAML for some other library or even an unsupported version of OpenSAML.
|
|
|
|
+
|
|
|
|
+As a natural outcome of the above two goals, Spring Security's SAML API is quite small relative to other modules.
|
|
|
|
+Instead, classes like `OpenSaml4AuthenticationRequestFactory` and `OpenSaml4AuthenticationProvider` expose `Converter` s that customize various steps in the authentication process.
|
|
|
|
+
|
|
|
|
+For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter will delegate to `OpenSaml4AuthenticationProvider`.
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+For backward compatibility, Spring Security will use the latest OpenSAML 3 by default.
|
|
|
|
+Note, though that OpenSAML 3 has reached it's end-of-life and updating to OpenSAML 4.x is recommended.
|
|
|
|
+For that reason, Spring Security supports both OpenSAML 3.x and 4.x.
|
|
|
|
+If you manage your OpenSAML dependency to 4.x, then Spring Security will select its OpenSAML 4.x implementations.
|
|
|
|
+
|
|
|
|
+.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 OpenSAML authentication provider.
|
|
|
|
+
|
|
|
|
+image:{icondir}/number_3.png[] The authentication provider deserializes the response into an OpenSAML `Response` and checks its signature.
|
|
|
|
+If the signature is invalid, authentication fails.
|
|
|
|
+
|
|
|
|
+image:{icondir}/number_4.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedAssertion` elements>>.
|
|
|
|
+If any decryptions fail, authentication fails.
|
|
|
|
+
|
|
|
|
+image:{icondir}/number_5.png[] Next, the provider validates the response's `Issuer` and `Destination` values.
|
|
|
|
+If they don't match what's in the `RelyingPartyRegistration`, authentication fails.
|
|
|
|
+
|
|
|
|
+image:{icondir}/number_6.png[] After that, the provider verifies the signature of each `Assertion`.
|
|
|
|
+If any signature is invalid, authentication fails.
|
|
|
|
+Also, if neither the response nor the assertions have signatures, authentication fails.
|
|
|
|
+Either the response or all the assertions must have signatures.
|
|
|
|
+
|
|
|
|
+image:{icondir}/number_7.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedID` or `EncryptedAttribute` elements>>.
|
|
|
|
+If any decryptions fail, authentication fails.
|
|
|
|
+
|
|
|
|
+image:{icondir}/number_8.png[] Next, the provider validates each assertion's `ExpiresAt` and `NotBefore` timestamps, the `<Subject>` and any `<AudienceRestriction>` conditions.
|
|
|
|
+If any validations fail, authentication fails.
|
|
|
|
+
|
|
|
|
+image:{icondir}/number_9.png[] Following that, the provider takes the first assertion's `AttributeStatement` and maps it to a `Map<String, List<Object>>`.
|
|
|
|
+It also grants the `ROLE_USER` granted authority.
|
|
|
|
+
|
|
|
|
+image:{icondir}/number_10.png[] And finally, it takes the `NameID` from the first assertion, the `Map` of attributes, and the `GrantedAuthority` and constructs a `Saml2AuthenticatedPrincipal`.
|
|
|
|
+Then, it places that principal and the authorities into a `Saml2Authentication`.
|
|
|
|
+
|
|
|
|
+The resulting `Authentication#getPrincipal` is a Spring Security `Saml2AuthenticatedPrincipal` object, and `Authentication#getName` maps to the first assertion's `NameID` element.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-opensaml-customization]]
|
|
|
|
+==== Customizing OpenSAML Configuration
|
|
|
|
+
|
|
|
|
+Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class, like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+static {
|
|
|
|
+ OpenSamlInitializationService.initialize();
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+companion object {
|
|
|
|
+ init {
|
|
|
|
+ OpenSamlInitializationService.initialize()
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+This replaces OpenSAML's `InitializationService#initialize`.
|
|
|
|
+
|
|
|
|
+Occasionally, it can be valuable to customize how OpenSAML builds, marshalls, and unmarshalls SAML objects.
|
|
|
|
+In these circumstances, you may instead want to call `OpenSamlInitializationService#requireInitialize(Consumer)` that gives you access to OpenSAML's `XMLObjectProviderFactory`.
|
|
|
|
+
|
|
|
|
+For example, when sending an unsigned AuthNRequest, you may want to force reauthentication.
|
|
|
|
+In that case, you can register your own `AuthnRequestMarshaller`, like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.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 only be called once per application instance.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-sansboot]]
|
|
|
|
+=== Overriding or Replacing Boot Auto Configuration
|
|
|
|
+
|
|
|
|
+There are two `@Bean` s that Spring Boot generates for a relying party.
|
|
|
|
+
|
|
|
|
+The first is a `WebSecurityConfigurerAdapter` that configures the app as a relying party.
|
|
|
|
+When including `spring-security-saml2-service-provider`, the `WebSecurityConfigurerAdapter` looks like:
|
|
|
|
+
|
|
|
|
+.Default JWT Configuration
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+protected void configure(HttpSecurity http) {
|
|
|
|
+ http
|
|
|
|
+ .authorizeRequests(authorize -> authorize
|
|
|
|
+ .anyRequest().authenticated()
|
|
|
|
+ )
|
|
|
|
+ .saml2Login(withDefaults());
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+fun configure(http: HttpSecurity) {
|
|
|
|
+ http {
|
|
|
|
+ authorizeRequests {
|
|
|
|
+ authorize(anyRequest, authenticated)
|
|
|
|
+ }
|
|
|
|
+ saml2Login { }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one.
|
|
|
|
+
|
|
|
|
+You can replace this by exposing the bean within the application:
|
|
|
|
+
|
|
|
|
+.Custom SAML 2.0 Login Configuration
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
|
|
|
+ protected void configure(HttpSecurity http) {
|
|
|
|
+ http
|
|
|
|
+ .authorizeRequests(authorize -> authorize
|
|
|
|
+ .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
|
|
|
|
+ .anyRequest().authenticated()
|
|
|
|
+ )
|
|
|
|
+ .saml2Login(withDefaults());
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
|
|
|
|
+ override fun configure(http: HttpSecurity) {
|
|
|
|
+ http {
|
|
|
|
+ authorizeRequests {
|
|
|
|
+ authorize("/messages/**", hasAuthority("ROLE_USER"))
|
|
|
|
+ authorize(anyRequest, authenticated)
|
|
|
|
+ }
|
|
|
|
+ saml2Login {
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+The above requires the role of `USER` for any URL that starts with `/messages/`.
|
|
|
|
+
|
|
|
|
+[[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 things like the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.
|
|
|
|
+
|
|
|
|
+You can override the default by publishing your own `RelyingPartyRegistrationRepository` bean.
|
|
|
|
+For example, you can look up the asserting party's configuration by hitting its metadata endpoint like so:
|
|
|
|
+
|
|
|
|
+.Relying Party Registration Repository
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@Value("${metadata.location}")
|
|
|
|
+String assertingPartyMetadataLocation;
|
|
|
|
+
|
|
|
|
+@Bean
|
|
|
|
+public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
|
|
|
|
+ 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)
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+Or you can provide each detail manually, as you can see below:
|
|
|
|
+
|
|
|
|
+.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]
|
|
|
|
+Note that `X509Support` is an OpenSAML class, used here in the snippet for brevity
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-relyingpartyregistrationrepository-dsl]]
|
|
|
|
+
|
|
|
|
+Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configured `WebSecurityConfigurerAdapter`:
|
|
|
|
+
|
|
|
|
+.Custom Relying Party Registration DSL
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
|
|
|
+ protected void configure(HttpSecurity http) {
|
|
|
|
+ http
|
|
|
|
+ .authorizeRequests(authorize -> authorize
|
|
|
|
+ .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
|
|
|
|
+ .anyRequest().authenticated()
|
|
|
|
+ )
|
|
|
|
+ .saml2Login(saml2 -> saml2
|
|
|
|
+ .relyingPartyRegistrationRepository(relyingPartyRegistrations())
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
|
|
|
|
+ override fun configure(http: HttpSecurity) {
|
|
|
|
+ http {
|
|
|
|
+ authorizeRequests {
|
|
|
|
+ authorize("/messages/**", hasAuthority("ROLE_USER"))
|
|
|
|
+ authorize(anyRequest, authenticated)
|
|
|
|
+ }
|
|
|
|
+ saml2Login {
|
|
|
|
+ relyingPartyRegistrationRepository = relyingPartyRegistrations()
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+[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 assering party's metadata.
|
|
|
|
+
|
|
|
|
+In a `RelyingPartyRegistration`, you can provide relying party metadata like its `Issuer` value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads.
|
|
|
|
+
|
|
|
|
+Also, you can provide asserting party metadata like its `Issuer` value, where it expects AuthnRequests to be sent to, and any public credentials that it owns for the purposes of the relying party verifying or encrypting payloads.
|
|
|
|
+
|
|
|
|
+The following `RelyingPartyRegistration` is the minimum required for most setups:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[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();
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+Though a more sophisticated setup is also possible, like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.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}+`.
|
|
|
|
+It's mapped by default to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-rpr-uripatterns]]
|
|
|
|
+==== URI Patterns
|
|
|
|
+
|
|
|
|
+You probably noticed in the above examples the `+{baseUrl}+` and `+{registrationId}+` placeholders.
|
|
|
|
+
|
|
|
|
+These are useful for generating URIs. As such, the relying party's `entityId` and `assertionConsumerServiceLocation` support the following placeholders:
|
|
|
|
+
|
|
|
|
+* `baseUrl` - the scheme, host, and port of a deployed application
|
|
|
|
+* `registrationId` - the registration id for this relying party
|
|
|
|
+* `baseScheme` - the scheme of a deployed application
|
|
|
|
+* `baseHost` - the host of a deployed application
|
|
|
|
+* `basePort` - the port of a deployed application
|
|
|
|
+
|
|
|
|
+For example, the `assertionConsumerServiceLocation` defined above was:
|
|
|
|
+
|
|
|
|
+`+/my-login-endpoint/{registrationId}+`
|
|
|
|
+
|
|
|
|
+which in a deployed application would translate to
|
|
|
|
+
|
|
|
|
+`+/my-login-endpoint/adfs+`
|
|
|
|
+
|
|
|
|
+The `entityId` above was defined as:
|
|
|
|
+
|
|
|
|
+`+{baseUrl}/{registrationId}+`
|
|
|
|
+
|
|
|
|
+which in a deployed application would translate to
|
|
|
|
+
|
|
|
|
+`+https://rp.example.com/adfs+`
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-rpr-credentials]]
|
|
|
|
+==== Credentials
|
|
|
|
+
|
|
|
|
+You also likely noticed the credential that was used.
|
|
|
|
+
|
|
|
|
+Oftentimes, a relying party will use the same key to sign payloads as well as decrypt them.
|
|
|
|
+Or it will use the same key to verify payloads as well as encrypt them.
|
|
|
|
+
|
|
|
|
+Because of this, Spring Security ships with `Saml2X509Credential`, a SAML-specific credential that simplifies configuring the same key for different use cases.
|
|
|
|
+
|
|
|
|
+At a minimum, it's necessary to have a certificate from the asserting party so that the asserting party's signed responses can be verified.
|
|
|
|
+
|
|
|
|
+To construct a `Saml2X509Credential` that you'll use to verify assertions from the asserting party, you can load the file and use
|
|
|
|
+the `CertificateFactory` like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+Resource resource = new ClassPathResource("ap.crt");
|
|
|
|
+try (InputStream is = resource.getInputStream()) {
|
|
|
|
+ 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?
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+Let's say that the asserting party is going to also encrypt the assertion.
|
|
|
|
+In that case, the relying party will need a private key to be able to decrypt the encrypted value.
|
|
|
|
+
|
|
|
|
+In that case, you'll need an `RSAPrivateKey` as well as its corresponding `X509Certificate`.
|
|
|
|
+You can load the first using Spring Security's `RsaKeyConverters` utility class and the second as you did before:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[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, then Spring Boot will perform these conversions for you.
|
|
|
|
+
|
|
|
|
+[[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:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+public class SingleRelyingPartyRegistrationResolver
|
|
|
|
+ implements Converter<HttpServletRequest, RelyingPartyRegistration> {
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public RelyingPartyRegistration convert(HttpServletRequest request) {
|
|
|
|
+ return this.relyingParty;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+class SingleRelyingPartyRegistrationResolver : Converter<HttpServletRequest?, RelyingPartyRegistration?> {
|
|
|
|
+ override fun convert(request: HttpServletRequest?): RelyingPartyRegistration? {
|
|
|
|
+ return this.relyingParty
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+Then, you can provide this resolver to the appropriate filters that <<servlet-saml2login-sp-initiated-factory, produce `<saml2:AuthnRequest>` s>>, <<servlet-saml2login-authenticate-responses, authenticate `<saml2:Response>` s>>, and <<servlet-saml2login-metadata, produce `<saml2:SPSSODescriptor>` metadata>>.
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-rpr-duplicated]]
|
|
|
|
+==== Duplicated Relying Party Configurations
|
|
|
|
+
|
|
|
|
+When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances:
|
|
|
|
+
|
|
|
|
+* The relying party's `entityId`
|
|
|
|
+* Its `assertionConsumerServiceLocation`, and
|
|
|
|
+* Its credentials, for example its signing or decryption credentials
|
|
|
|
+
|
|
|
|
+What's nice about this setup is credentials may be more easily rotated for some identity providers vs others.
|
|
|
|
+
|
|
|
|
+The duplication can be alleviated in a few different ways.
|
|
|
|
+
|
|
|
|
+First, in YAML this can be alleviated with references, like so:
|
|
|
|
+
|
|
|
|
+[source,yaml]
|
|
|
|
+----
|
|
|
|
+spring:
|
|
|
|
+ security:
|
|
|
|
+ saml2:
|
|
|
|
+ relyingparty:
|
|
|
|
+ okta:
|
|
|
|
+ signing.credentials: &relying-party-credentials
|
|
|
|
+ - private-key-location: classpath:rp.key
|
|
|
|
+ - certificate-location: classpath:rp.crt
|
|
|
|
+ identityprovider:
|
|
|
|
+ entity-id: ...
|
|
|
|
+ azure:
|
|
|
|
+ signing.credentials: *relying-party-credentials
|
|
|
|
+ identityprovider:
|
|
|
|
+ entity-id: ...
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+Second, in a database, it's not necessary to replicate `RelyingPartyRegistration` 's model.
|
|
|
|
+
|
|
|
|
+Third, in Java, you can create a custom configuration method, like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.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-sp-initiated-factory]]
|
|
|
|
+=== Producing `<saml2:AuthnRequest>` s
|
|
|
|
+
|
|
|
|
+As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party.
|
|
|
|
+
|
|
|
|
+Spring Security achieves this in part by registering the `Saml2WebSsoAuthenticationRequestFilter` in the filter chain.
|
|
|
|
+This filter by default responds to endpoint `+/saml2/authenticate/{registrationId}+`.
|
|
|
|
+
|
|
|
|
+For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to:
|
|
|
|
+
|
|
|
|
+`https://rp.example.org/saml2/authenticate/ping`
|
|
|
|
+
|
|
|
|
+and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
|
|
|
|
+
|
|
|
|
+[[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)
|
|
|
|
+ )
|
|
|
|
+ .build();
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,java,role="secondary"]
|
|
|
|
+----
|
|
|
|
+var relyingPartyRegistration: RelyingPartyRegistration =
|
|
|
|
+ RelyingPartyRegistration.withRegistrationId("okta")
|
|
|
|
+ // ...
|
|
|
|
+ .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
|
|
|
|
+ // ...
|
|
|
|
+ .wantAuthnRequestsSigned(false)
|
|
|
|
+ }
|
|
|
|
+ .build();
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+Otherwise, you will need to specify a private key to `RelyingPartyRegistration#signingX509Credentials` so that Spring Security can sign the `<saml2:AuthnRequest>` before sending.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-sp-initiated-factory-algorithm]]
|
|
|
|
+By default, Spring Security will sign the `<saml2:AuthnRequest>` using `rsa-sha256`, though some asserting parties will require a different algorithm, as indicated in their metadata.
|
|
|
|
+
|
|
|
|
+You can configure the algorithm based on the asserting party's <<servlet-saml2login-relyingpartyregistrationrepository,metadata using `RelyingPartyRegistrations`>>.
|
|
|
|
+
|
|
|
|
+Or, you can provide it manually:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+String metadataLocation = "classpath:asserting-party-metadata.xml";
|
|
|
|
+RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
|
|
|
|
+ // ...
|
|
|
|
+ .assertingPartyDetails((party) -> party
|
|
|
|
+ // ...
|
|
|
|
+ .signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
|
|
|
|
+ )
|
|
|
|
+ .build();
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+var metadataLocation = "classpath:asserting-party-metadata.xml"
|
|
|
|
+var relyingPartyRegistration: RelyingPartyRegistration =
|
|
|
|
+ RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
|
|
|
|
+ // ...
|
|
|
|
+ .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
|
|
|
|
+ // ...
|
|
|
|
+ .signingAlgorithms { sign: MutableList<String?> ->
|
|
|
|
+ sign.add(
|
|
|
|
+ SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ .build();
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+NOTE: The snippet above uses the OpenSAML `SignatureConstants` class to supply the algorithm name.
|
|
|
|
+But, that's just for convenience.
|
|
|
|
+Since the datatype is `String`, you can supply the name of the algorithm directly.
|
|
|
|
+
|
|
|
|
+[[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:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
|
|
|
|
+ // ...
|
|
|
|
+ .assertingPartyDetails(party -> party
|
|
|
|
+ // ...
|
|
|
|
+ .singleSignOnServiceBinding(Saml2MessageBinding.POST)
|
|
|
|
+ )
|
|
|
|
+ .build();
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+var relyingPartyRegistration: RelyingPartyRegistration? =
|
|
|
|
+ RelyingPartyRegistration.withRegistrationId("okta")
|
|
|
|
+ // ...
|
|
|
|
+ .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
|
|
|
|
+ // ...
|
|
|
|
+ .singleSignOnServiceBinding(Saml2MessageBinding.POST)
|
|
|
|
+ }
|
|
|
|
+ .build()
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+[[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`.
|
|
|
|
+For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
|
|
|
|
+
|
|
|
|
+If you don't need information from the `HttpServletRequest` to make your decision, then the easiest way is to <<servlet-saml2login-opensaml-customization,register a custom `AuthnRequestMarshaller` with OpenSAML>>.
|
|
|
|
+This will give you access to post-process the `AuthnRequest` instance before it's serialized.
|
|
|
|
+
|
|
|
|
+But, if you do need something from the request, then you can use create a custom `Saml2AuthenticationRequestContext` implementation and then a `Converter<Saml2AuthenticationRequestContext, AuthnRequest>` to build an `AuthnRequest` yourself, like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@Component
|
|
|
|
+public class AuthnRequestConverter implements
|
|
|
|
+ Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
|
|
|
|
+
|
|
|
|
+ private final AuthnRequestBuilder authnRequestBuilder;
|
|
|
|
+ private final IssuerBuilder issuerBuilder;
|
|
|
|
+
|
|
|
|
+ // ... constructor
|
|
|
|
+
|
|
|
|
+ public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
|
|
|
|
+ MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context;
|
|
|
|
+ Issuer issuer = issuerBuilder.buildObject();
|
|
|
|
+ issuer.setValue(myContext.getIssuer());
|
|
|
|
+
|
|
|
|
+ AuthnRequest authnRequest = authnRequestBuilder.buildObject();
|
|
|
|
+ authnRequest.setIssuer(issuer);
|
|
|
|
+ authnRequest.setDestination(myContext.getDestination());
|
|
|
|
+ authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl());
|
|
|
|
+
|
|
|
|
+ // ... additional settings
|
|
|
|
+
|
|
|
|
+ authRequest.setForceAuthn(myContext.getForceAuthn());
|
|
|
|
+ return authnRequest;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+@Component
|
|
|
|
+class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
|
|
|
|
+ private val authnRequestBuilder: AuthnRequestBuilder? = null
|
|
|
|
+ private val issuerBuilder: IssuerBuilder? = null
|
|
|
|
+
|
|
|
|
+ // ... constructor
|
|
|
|
+ override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
|
|
|
|
+ val myContext: MySaml2AuthenticationRequestContext = context
|
|
|
|
+ val issuer: Issuer = issuerBuilder.buildObject()
|
|
|
|
+ issuer.value = myContext.getIssuer()
|
|
|
|
+ val authnRequest: AuthnRequest = authnRequestBuilder.buildObject()
|
|
|
|
+ authnRequest.issuer = issuer
|
|
|
|
+ authnRequest.destination = myContext.getDestination()
|
|
|
|
+ authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl()
|
|
|
|
+
|
|
|
|
+ // ... additional settings
|
|
|
|
+ authRequest.setForceAuthn(myContext.getForceAuthn())
|
|
|
|
+ return authnRequest
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as `@Bean` s:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@Bean
|
|
|
|
+Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() {
|
|
|
|
+ Saml2AuthenticationRequestContextResolver resolver =
|
|
|
|
+ new DefaultSaml2AuthenticationRequestContextResolver();
|
|
|
|
+ return request -> {
|
|
|
|
+ Saml2AuthenticationRequestContext context = resolver.resolve(request);
|
|
|
|
+ return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null);
|
|
|
|
+ };
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+@Bean
|
|
|
|
+Saml2AuthenticationRequestFactory authenticationRequestFactory(
|
|
|
|
+ AuthnRequestConverter authnRequestConverter) {
|
|
|
|
+
|
|
|
|
+ OpenSaml4AuthenticationRequestFactory authenticationRequestFactory =
|
|
|
|
+ new OpenSaml4AuthenticationRequestFactory();
|
|
|
|
+ authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
|
|
|
|
+ return authenticationRequestFactory;
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+@Bean
|
|
|
|
+open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver {
|
|
|
|
+ val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver()
|
|
|
|
+ return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest ->
|
|
|
|
+ val context = resolver.resolve(request)
|
|
|
|
+ MySaml2AuthenticationRequestContext(
|
|
|
|
+ context,
|
|
|
|
+ request.getParameter("force") != null
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+@Bean
|
|
|
|
+open fun authenticationRequestFactory(
|
|
|
|
+ authnRequestConverter: AuthnRequestConverter?
|
|
|
|
+): Saml2AuthenticationRequestFactory? {
|
|
|
|
+ val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory()
|
|
|
|
+ authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter)
|
|
|
|
+ return authenticationRequestFactory
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-authenticate-responses]]
|
|
|
|
+=== Authenticating `<saml2:Response>` s
|
|
|
|
+
|
|
|
|
+To verify SAML 2.0 Responses, Spring Security uses <<servlet-saml2login-architecture,`OpenSaml4AuthenticationProvider`>> by default.
|
|
|
|
+
|
|
|
|
+You can configure this in a number of ways including:
|
|
|
|
+
|
|
|
|
+1. Setting a clock skew to timestamp validation
|
|
|
|
+2. Mapping the response to a list of `GrantedAuthority` instances
|
|
|
|
+3. Customizing the strategy for validating assertions
|
|
|
|
+4. Customizing the strategy for decrypting response and assertion elements
|
|
|
|
+
|
|
|
|
+To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
|
|
|
|
+
|
|
|
|
+[[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.
|
|
|
|
+For that reason, you can configure `OpenSaml4AuthenticationProvider` 's default assertion validator with some tolerance:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ protected void configure(HttpSecurity http) throws Exception {
|
|
|
|
+ OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
|
|
|
|
+ authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
|
|
|
|
+ .createDefaultAssertionValidator(assertionToken -> {
|
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
|
+ params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
|
|
|
|
+ // ... other validation parameters
|
|
|
|
+ return new ValidationContext(params);
|
|
|
|
+ })
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ http
|
|
|
|
+ .authorizeRequests(authz -> authz
|
|
|
|
+ .anyRequest().authenticated()
|
|
|
|
+ )
|
|
|
|
+ .saml2Login(saml2 -> saml2
|
|
|
|
+ .authenticationManager(new ProviderManager(authenticationProvider))
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+open class SecurityConfig : WebSecurityConfigurerAdapter() {
|
|
|
|
+ override fun configure(http: HttpSecurity) {
|
|
|
|
+ val authenticationProvider = OpenSaml4AuthenticationProvider()
|
|
|
|
+ authenticationProvider.setAssertionValidator(
|
|
|
|
+ OpenSaml4AuthenticationProvider
|
|
|
|
+ .createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
|
|
|
|
+ val params: MutableMap<String, Any> = HashMap()
|
|
|
|
+ params[CLOCK_SKEW] =
|
|
|
|
+ Duration.ofMinutes(10).toMillis()
|
|
|
|
+ ValidationContext(params)
|
|
|
|
+ })
|
|
|
|
+ )
|
|
|
|
+ http {
|
|
|
|
+ authorizeRequests {
|
|
|
|
+ authorize(anyRequest, authenticated)
|
|
|
|
+ }
|
|
|
|
+ saml2Login {
|
|
|
|
+ authenticationManager = ProviderManager(authenticationProvider)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
|
|
|
|
+==== Coordinating with a `UserDetailsService`
|
|
|
|
+
|
|
|
|
+Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
|
|
|
|
+In that case, the response authentication converter can come in handy, as can be seen below:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
|
+ @Autowired
|
|
|
|
+ UserDetailsService userDetailsService;
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ protected void configure(HttpSecurity http) throws Exception {
|
|
|
|
+ OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
|
|
|
|
+ authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
|
|
|
|
+ Saml2Authentication authentication = OpenSaml4AuthenticationProvider
|
|
|
|
+ .createDefaultResponseAuthenticationConverter() <1>
|
|
|
|
+ .convert(responseToken);
|
|
|
|
+ Assertion assertion = responseToken.getResponse().getAssertions().get(0);
|
|
|
|
+ String username = assertion.getSubject().getNameID().getValue();
|
|
|
|
+ UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); <2>
|
|
|
|
+ return MySaml2Authentication(userDetails, authentication); <3>
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ http
|
|
|
|
+ .authorizeRequests(authz -> authz
|
|
|
|
+ .anyRequest().authenticated()
|
|
|
|
+ )
|
|
|
|
+ .saml2Login(saml2 -> saml2
|
|
|
|
+ .authenticationManager(new ProviderManager(authenticationProvider))
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+open class SecurityConfig : WebSecurityConfigurerAdapter() {
|
|
|
|
+ @Autowired
|
|
|
|
+ var userDetailsService: UserDetailsService? = null
|
|
|
|
+
|
|
|
|
+ override fun configure(http: HttpSecurity) {
|
|
|
|
+ val authenticationProvider = OpenSaml4AuthenticationProvider()
|
|
|
|
+ authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
|
|
|
|
+ val authentication = OpenSaml4AuthenticationProvider
|
|
|
|
+ .createDefaultResponseAuthenticationConverter() <1>
|
|
|
|
+ .convert(responseToken)
|
|
|
|
+ val assertion: Assertion = responseToken.response.assertions[0]
|
|
|
|
+ val username: String = assertion.subject.nameID.value
|
|
|
|
+ val userDetails = userDetailsService!!.loadUserByUsername(username) <2>
|
|
|
|
+ MySaml2Authentication(userDetails, authentication) <3>
|
|
|
|
+ }
|
|
|
|
+ http {
|
|
|
|
+ authorizeRequests {
|
|
|
|
+ authorize(anyRequest, authenticated)
|
|
|
|
+ }
|
|
|
|
+ saml2Login {
|
|
|
|
+ authenticationManager = ProviderManager(authenticationProvider)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+<1> First, call the default converter, which extracts attributes and authorities from the response
|
|
|
|
+<2> Second, call the <<servlet-authentication-userdetailsservice, `UserDetailsService`>> using the relevant information
|
|
|
|
+<3> Third, return a custom authentication that includes the user details
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+It's not required to call `OpenSaml4AuthenticationProvider` 's default authentication converter.
|
|
|
|
+It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from `AttributeStatement` s as well as the single `ROLE_USER` authority.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
|
|
|
|
+==== Performing Additional Response Validation
|
|
|
|
+
|
|
|
|
+`OpenSaml4AuthenticationProvider` validates the `Issuer` and `Destination` values right after decrypting the `Response`.
|
|
|
|
+You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.
|
|
|
|
+
|
|
|
|
+For example, you can throw a custom exception with any additional information available in the `Response` object, like so:
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
|
|
|
|
+provider.setResponseValidator((responseToken) -> {
|
|
|
|
+ Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
|
|
|
|
+ .createDefaultResponseValidator()
|
|
|
|
+ .convert(responseToken)
|
|
|
|
+ .concat(myCustomValidator.convert(responseToken));
|
|
|
|
+ if (!result.getErrors().isEmpty()) {
|
|
|
|
+ String inResponseTo = responseToken.getInResponseTo();
|
|
|
|
+ throw new CustomSaml2AuthenticationException(result, inResponseTo);
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+});
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+==== Performing Additional Assertion Validation
|
|
|
|
+`OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
|
|
|
|
+After verifying the signature, it will:
|
|
|
|
+
|
|
|
|
+1. Validate `<AudienceRestriction>` and `<DelegationRestriction>` conditions
|
|
|
|
+2. Validate `<SubjectConfirmation>` s, expect for any IP address information
|
|
|
|
+
|
|
|
|
+To perform additional validation, you can configure your own assertion validator that delegates to `OpenSaml4AuthenticationProvider` 's default and then performs its own.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]]
|
|
|
|
+For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
|
|
|
|
+OneTimeUseConditionValidator validator = ...;
|
|
|
|
+provider.setAssertionValidator(assertionToken -> {
|
|
|
|
+ Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
|
|
|
|
+ .createDefaultAssertionValidator()
|
|
|
|
+ .convert(assertionToken);
|
|
|
|
+ Assertion assertion = assertionToken.getAssertion();
|
|
|
|
+ OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
|
|
|
|
+ ValidationContext context = new ValidationContext();
|
|
|
|
+ try {
|
|
|
|
+ if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
+ return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
|
|
|
|
+ }
|
|
|
|
+ return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
|
|
|
|
+});
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+var provider = OpenSaml4AuthenticationProvider()
|
|
|
|
+var validator: OneTimeUseConditionValidator = ...
|
|
|
|
+provider.setAssertionValidator { assertionToken ->
|
|
|
|
+ val result = OpenSaml4AuthenticationProvider
|
|
|
|
+ .createDefaultAssertionValidator()
|
|
|
|
+ .convert(assertionToken)
|
|
|
|
+ val assertion: Assertion = assertionToken.assertion
|
|
|
|
+ val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
|
|
|
|
+ val context = ValidationContext()
|
|
|
|
+ try {
|
|
|
|
+ if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
|
|
|
|
+ return@setAssertionValidator result
|
|
|
|
+ }
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
+ return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
|
|
|
|
+ }
|
|
|
|
+ result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+While recommended, it's not necessary to call `OpenSaml4AuthenticationProvider` 's default assertion validator.
|
|
|
|
+A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-opensamlauthenticationprovider-decryption]]
|
|
|
|
+==== Customizing Decryption
|
|
|
|
+
|
|
|
|
+Spring Security decrypts `<saml2:EncryptedAssertion>`, `<saml2:EncryptedAttribute>`, and `<saml2:EncryptedID>` elements automatically by using the decryption <<servlet-saml2login-rpr-credentials,`Saml2X509Credential` instances>> registered in the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>.
|
|
|
|
+
|
|
|
|
+`OpenSaml4AuthenticationProvider` exposes <<servlet-saml2login-architecture,two decryption strategies>>.
|
|
|
|
+The response decrypter is for decrypting encrypted elements of the `<saml2:Response>`, like `<saml2:EncryptedAssertion>`.
|
|
|
|
+The assertion decrypter is for decrypting encrypted elements of the `<saml2:Assertion>`, like `<saml2:EncryptedAttribute>` and `<saml2:EncryptedID>`.
|
|
|
|
+
|
|
|
|
+You can replace `OpenSaml4AuthenticationProvider`'s default decryption strategy with your own.
|
|
|
|
+For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>`, you can use it instead like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+MyDecryptionService decryptionService = ...;
|
|
|
|
+OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
|
|
|
|
+provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+val decryptionService: MyDecryptionService = ...
|
|
|
|
+val provider = OpenSaml4AuthenticationProvider()
|
|
|
|
+provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+If you are also decrypting individual elements in a `<saml2:Assertion>`, you can customize the assertion decrypter, too:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+NOTE: There are two separate decrypters since assertions can be signed separately from responses.
|
|
|
|
+Trying to decrypt a signed assertion's elements before signature verification may invalidate the signature.
|
|
|
|
+If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-authenticationmanager-custom]]
|
|
|
|
+==== Using a Custom Authentication Manager
|
|
|
|
+
|
|
|
|
+[[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.
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ protected void configure(HttpSecurity http) throws Exception {
|
|
|
|
+ AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
|
|
|
|
+ http
|
|
|
|
+ .authorizeRequests(authorize -> authorize
|
|
|
|
+ .anyRequest().authenticated()
|
|
|
|
+ )
|
|
|
|
+ .saml2Login(saml2 -> saml2
|
|
|
|
+ .authenticationManager(authenticationManager)
|
|
|
|
+ )
|
|
|
|
+ ;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+open class SecurityConfig : WebSecurityConfigurerAdapter() {
|
|
|
|
+ override fun configure(http: HttpSecurity) {
|
|
|
|
+ val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
|
|
|
|
+ http {
|
|
|
|
+ authorizeRequests {
|
|
|
|
+ authorize(anyRequest, authenticated)
|
|
|
|
+ }
|
|
|
|
+ saml2Login {
|
|
|
|
+ authenticationManager = customAuthenticationManager
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-authenticatedprincipal]]
|
|
|
|
+=== Using `Saml2AuthenticatedPrincipal`
|
|
|
|
+
|
|
|
|
+With the relying party correctly configured for a given asserting party, it's ready to accept assertions.
|
|
|
|
+Once the relying party validates an assertion, the result is a `Saml2Authentication` with a `Saml2AuthenticatedPrincipal`.
|
|
|
|
+
|
|
|
|
+This means that you can access the principal in your controller like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@Controller
|
|
|
|
+public class MainController {
|
|
|
|
+ @GetMapping("/")
|
|
|
|
+ public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
|
|
|
|
+ String email = principal.getFirstAttribute("email");
|
|
|
|
+ model.setAttribute("email", email);
|
|
|
|
+ return "index";
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+@Controller
|
|
|
|
+class MainController {
|
|
|
|
+ @GetMapping("/")
|
|
|
|
+ fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
|
|
|
|
+ val email = principal.getFirstAttribute<String>("email")
|
|
|
|
+ model.setAttribute("email", email)
|
|
|
|
+ return "index"
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+[TIP]
|
|
|
|
+Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call `getAttribute` to get the list of attributes or `getFirstAttribute` to get the first in the list.
|
|
|
|
+`getFirstAttribute` is quite handy when you know that there is only one value.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-metadata]]
|
|
|
|
+=== Producing `<saml2:SPSSODescriptor>` Metadata
|
|
|
|
+
|
|
|
|
+You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+Converter<HttpServletRequest, RelyingPartyRegistration> relyingPartyRegistrationResolver =
|
|
|
|
+ new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
|
|
|
|
+Saml2MetadataFilter filter = new Saml2MetadataFilter(
|
|
|
|
+ relyingPartyRegistrationResolver,
|
|
|
|
+ new OpenSamlMetadataResolver());
|
|
|
|
+
|
|
|
|
+http
|
|
|
|
+ // ...
|
|
|
|
+ .saml2Login(withDefaults())
|
|
|
|
+ .addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+val relyingPartyRegistrationResolver: Converter<HttpServletRequest, RelyingPartyRegistration> =
|
|
|
|
+ DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)
|
|
|
|
+val filter = Saml2MetadataFilter(
|
|
|
|
+ relyingPartyRegistrationResolver,
|
|
|
|
+ OpenSamlMetadataResolver()
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+http {
|
|
|
|
+ //...
|
|
|
|
+ saml2Login { }
|
|
|
|
+ addFilterBefore<Saml2WebSsoAuthenticationFilter>(filter)
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+You can use this metadata endpoint to register your relying party with your asserting party.
|
|
|
|
+This is often as simple as finding the correct form field to supply the metadata endpoint.
|
|
|
|
+
|
|
|
|
+By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`.
|
|
|
|
+You can change this by calling the `setRequestMatcher` method on the filter:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"))
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+ensuring that the `registrationId` hint is at the end of the path.
|
|
|
|
+
|
|
|
|
+Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a `registrationId` hint, like so:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+filter.setRequestMatcher(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`:
|
|
|
|
+
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+http
|
|
|
|
+ // ...
|
|
|
|
+ .logout(logout -> logout
|
|
|
|
+ .logoutSuccessHandler(myCustomSuccessHandler())
|
|
|
|
+ .logoutRequestMatcher(myRequestMatcher())
|
|
|
|
+ )
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
|
|
+----
|
|
|
|
+http {
|
|
|
|
+ logout {
|
|
|
|
+ // ...
|
|
|
|
+ logoutSuccessHandler = myCustomSuccessHandler()
|
|
|
|
+ logoutRequestMatcher = myRequestMatcher()
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+The success handler will send logout requests to the asserting party.
|
|
|
|
+
|
|
|
|
+The request matcher will detect logout requests from the asserting party.
|
|
|
|
+
|