diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 16bd85cc80..4b009d9193 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -63,6 +63,9 @@ **** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy] **** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens] ** xref:servlet/saml2/index.adoc[SAML2] +*** xref:servlet/saml2/login.adoc[SAML2 Log In] +*** xref:servlet/saml2/logout.adoc[SAML2 Logout] +*** xref:servlet/saml2/metadata.adoc[SAML2 Metadata] ** xref:servlet/exploits/index.adoc[Protection Against Exploits] *** xref:servlet/exploits/csrf.adoc[] *** xref:servlet/exploits/headers.adoc[] diff --git a/docs/modules/ROOT/pages/servlet/saml2/index.adoc b/docs/modules/ROOT/pages/servlet/saml2/index.adoc index 472ff1887a..5f7b1c43a2 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/index.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/index.adoc @@ -1,1617 +1,6 @@ [[servlet-saml2]] = SAML2 -:figures: servlet/saml2 +:page-section-summary-toc: 1 Spring Security provides comprehensive SAML 2 support. This section discusses how to integrate SAML 2 into your servlet based application. - -[[servlet-saml2login]] -== SAML 2.0 Login - -The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users https://wiki.shibboleth.net/confluence/display/CONCEPT/FlowsAndConfig[log in] to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc). - -NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in -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 xref:servlet/oauth2/index.adoc[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 xref:servlet/oauth2/oauth2-login.adoc#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 xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] and xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[ `AbstractAuthenticationProcessingFilter`] diagrams: - -image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized. - -image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. - -image:{icondir}/number_3.png[] Since the user lacks authorization, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__. -The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`] which redirects to <` generating endpoint>>, `Saml2WebSsoAuthenticationRequestFilter`. -Or, if you've <>, it will first redirect to a picker page. - -image:{icondir}/number_4.png[] Next, the `Saml2WebSsoAuthenticationRequestFilter` creates, signs, serializes, and encodes a `` using its configured <>. - -image:{icondir}/number_5.png[] Then, the browser takes this `` and presents it to the asserting party. -The asserting party attempts to authentication the user. -If successful, it will return a `` back to the browser. - -image:{icondir}/number_6.png[] The browser then POSTs the `` to the assertion consumer service endpoint. - -[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]] -.Authenticating a `` -image::{figures}/saml2webssoauthenticationfilter.png[] - -The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram. - -image:{icondir}/number_1.png[] When the browser submits a `` to the application, it <>. -This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`. -This converter additionally resolves the <> and supplies it to `Saml2AuthenticationToken`. - -image:{icondir}/number_2.png[] Next, the filter passes the token to its configured xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`]. -By default, it will use the <>. - -image:{icondir}/number_3.png[] If authentication fails, then __Failure__ - -* The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[ `SecurityContextHolder`] is cleared out. -* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is invoked to restart the authentication process. - -image:{icondir}/number_4.png[] If authentication is successful, then __Success__. - -* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[ `Authentication`] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[ `SecurityContextHolder`]. -* The `Saml2WebSsoAuthenticationFilter` invokes `FilterChain#doFilter(request,response)` to continue with the rest of the application logic. - -[[servlet-saml2login-minimaldependencies]] -=== Minimal Dependencies - -SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`. -It builds off of the OpenSAML library. - -[[servlet-saml2login-minimalconfiguration]] -=== Minimal Configuration - -When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a service provider consists of two basic steps. -First, include the needed dependencies and second, indicate the necessary asserting party metadata. - -[NOTE] -Also, this presupposes that you've already <>. - -==== 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 - -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 <>. - -image:{icondir}/number_1.png[] The `Saml2WebSsoAuthenticationFilter` formulates the `Saml2AuthenticationToken` and invokes the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`]. - -image:{icondir}/number_2.png[] The xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`] invokes the OpenSAML authentication provider. - -image:{icondir}/number_3.png[] The authentication provider deserializes the response into an OpenSAML `Response` and checks its signature. -If the signature is invalid, authentication fails. - -image:{icondir}/number_4.png[] Then, the provider <>. -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 <>. -If any decryptions fail, authentication fails. - -image:{icondir}/number_8.png[] Next, the provider validates each assertion's `ExpiresAt` and `NotBefore` timestamps, the `` and any `` 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>`. -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 -> - 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 -> - 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 <> 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`. -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 { - - @Override - public RelyingPartyRegistration convert(HttpServletRequest request) { - return this.relyingParty; - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -class SingleRelyingPartyRegistrationResolver : Converter { - override fun convert(request: HttpServletRequest?): RelyingPartyRegistration? { - return this.relyingParty - } -} ----- -==== - -Then, you can provide this resolver to the appropriate filters that <` s>>, <` s>>, and <` 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 -> - 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 `` s - -As stated earlier, Spring Security's SAML 2.0 support produces a `` 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 ``. - -[[servlet-saml2login-sp-initiated-factory-signing]] -==== Changing How the `` Gets Sent - -By default, Spring Security signs each `` and send it as a GET to the asserting party. - -Many asserting parties don't require a signed ``. -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 `` before sending. - -[[servlet-saml2login-sp-initiated-factory-algorithm]] -By default, Spring Security will sign the `` 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 <>. - -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 -> - 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 `` 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 <>. -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` to build an `AuthnRequest` yourself, like so: - -==== -.Java -[source,java,role="primary"] ----- -@Component -public class AuthnRequestConverter implements - Converter { - - 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 { - 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 `` s - -To verify SAML 2.0 Responses, Spring Security uses <> 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 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 { - val params: MutableMap = 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 xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[ `UserDetailsService`] using the relevant information -<3> Third, return a custom authentication that includes the user details - -[NOTE] -It's not required to call `OpenSaml4AuthenticationProvider` 's default authentication converter. -It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from `AttributeStatement` s as well as the single `ROLE_USER` authority. - -[[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 `` and `` conditions -2. Validate `` 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 `` 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 `` or the `` since you are doing those yourself. - -[[servlet-saml2login-opensamlauthenticationprovider-decryption]] -==== Customizing Decryption - -Spring Security decrypts ``, ``, and `` elements automatically by using the decryption <> registered in the <>. - -`OpenSaml4AuthenticationProvider` exposes <>. -The response decrypter is for decrypting encrypted elements of the ``, like ``. -The assertion decrypter is for decrypting encrypted elements of the ``, like `` and ``. - -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 ``, 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 ``, 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("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 `` 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 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 = - DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository) -val filter = Saml2MetadataFilter( - relyingPartyRegistrationResolver, - OpenSamlMetadataResolver() -) - -http { - //... - saml2Login { } - addFilterBefore(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. - diff --git a/docs/modules/ROOT/pages/servlet/saml2/saml2-login.adoc b/docs/modules/ROOT/pages/servlet/saml2/login.adoc similarity index 80% rename from docs/modules/ROOT/pages/servlet/saml2/saml2-login.adoc rename to docs/modules/ROOT/pages/servlet/saml2/login.adoc index 1259f73bc5..83007d0b09 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/saml2-login.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login.adoc @@ -1,7 +1,5 @@ - - [[servlet-saml2login]] -== SAML 2.0 Login += SAML 2.0 Login :figures: images/servlet/saml2 :icondir: images/icons @@ -69,21 +67,21 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__ * The `Saml2WebSsoAuthenticationFilter` invokes `FilterChain#doFilter(request,response)` to continue with the rest of the application logic. [[servlet-saml2login-minimaldependencies]] -=== Minimal Dependencies +== 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 +== 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 <>. +Also, this presupposes that you've already xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[registered the relying party with your asserting party]. -==== Specifying Identity Provider Metadata +=== Specifying Identity Provider Metadata In a Spring Boot application, to specify an identity provider's metadata, simply do: @@ -116,7 +114,7 @@ And that's it! 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 +=== Runtime Expectations As configured above, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter: @@ -141,7 +139,7 @@ From here, consider jumping to: * <> [[servlet-saml2login-architecture]] -=== How SAML 2.0 Login Integrates with OpenSAML +== How SAML 2.0 Login Integrates with OpenSAML Spring Security's SAML 2.0 support has a couple of design goals: @@ -201,7 +199,7 @@ The resulting `Authentication#getPrincipal` is a Spring Security `Saml2Authentic `Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId` holds the <>. [[servlet-saml2login-opensaml-customization]] -==== Customizing OpenSAML Configuration +=== Customizing OpenSAML Configuration Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class, like so: @@ -293,7 +291,7 @@ companion object { The `requireInitialize` method may only be called once per application instance. [[servlet-saml2login-sansboot]] -=== Overriding or Replacing Boot Auto Configuration +== Overriding or Replacing Boot Auto Configuration There are two ``@Bean``s that Spring Boot generates for a relying party. @@ -524,7 +522,7 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() { A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`. [[servlet-saml2login-relyingpartyregistration]] -=== 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. @@ -618,7 +616,7 @@ The default for the `assertionConsumerServiceLocation` is `+/login/saml2/sso/{re It's mapped by default to <> in the filter chain. [[servlet-saml2login-rpr-uripatterns]] -==== URI Patterns +=== URI Patterns You probably noticed in the above examples the `+{baseUrl}+` and `+{registrationId}+` placeholders. @@ -647,7 +645,7 @@ which in a deployed application would translate to `+https://rp.example.com/adfs+` [[servlet-saml2login-rpr-credentials]] -==== Credentials +=== Credentials You also likely noticed the credential that was used. @@ -719,7 +717,7 @@ resource.inputStream.use { 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 +=== 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. @@ -763,13 +761,13 @@ class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationR ---- ==== -Then, you can provide this resolver to the appropriate filters that <``s>>, <``s>>, and <` metadata>>. +Then, you can provide this resolver to the appropriate filters that <``s>>, <``s>>, and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `` 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 +=== Duplicated Relying Party Configurations When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances: @@ -864,7 +862,7 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? { ==== [[servlet-saml2login-sp-initiated-factory]] -=== Producing ````s +== Producing ````s As stated earlier, Spring Security's SAML 2.0 support produces a `` to commence authentication with the asserting party. @@ -878,7 +876,7 @@ For example, if you were deployed to `https://rp.example.com` and you gave your and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded ``. [[servlet-saml2login-store-authn-request]] -==== Changing How the `` Gets Stored +=== Changing How the `` Gets Stored `Saml2WebSsoAuthenticationRequestFilter` uses an `Saml2AuthenticationRequestRepository` to persist an `AbstractSaml2AuthenticationRequest` instance before <`>> to the asserting party. @@ -909,7 +907,7 @@ open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository ==== [[servlet-saml2login-sp-initiated-factory-signing]] -==== Changing How the `` Gets Sent +=== Changing How the `` Gets Sent By default, Spring Security signs each `` and send it as a GET to the asserting party. @@ -1036,7 +1034,7 @@ var relyingPartyRegistration: RelyingPartyRegistration? = ==== [[servlet-saml2login-sp-initiated-factory-custom-authnrequest]] -==== Customizing OpenSAML's `AuthnRequest` Instance +=== 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. @@ -1157,7 +1155,7 @@ open fun authenticationRequestFactory( ==== [[servlet-saml2login-authenticate-responses]] -=== Authenticating ````s +== Authenticating ````s To verify SAML 2.0 Responses, Spring Security uses <> by default. @@ -1171,7 +1169,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-saml2login-opensamlauthenticationprovider-clockskew]] -==== Setting a Clock Skew +=== 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: @@ -1236,7 +1234,7 @@ open class SecurityConfig : WebSecurityConfigurerAdapter() { ==== [[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]] -==== Coordinating with a `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: @@ -1314,7 +1312,7 @@ It's not required to call `OpenSaml4AuthenticationProvider` 's default authentic 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 +=== 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. @@ -1336,7 +1334,7 @@ provider.setResponseValidator((responseToken) -> { }); ---- -==== Performing Additional Assertion Validation +=== Performing Additional Assertion Validation `OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions. After verifying the signature, it will: @@ -1362,7 +1360,7 @@ provider.setAssertionValidator(assertionToken -> { OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse(); ValidationContext context = new ValidationContext(); try { - if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) { + if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) { return result; } } catch (Exception e) { @@ -1385,7 +1383,7 @@ provider.setAssertionValidator { assertionToken -> val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse val context = ValidationContext() try { - if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) { + if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) { return@setAssertionValidator result } } catch (e: Exception) { @@ -1401,7 +1399,7 @@ While recommended, it's not necessary to call `OpenSaml4AuthenticationProvider` A circumstance where you would skip it would be if you don't need it to check the `` or the `` since you are doing those yourself. [[servlet-saml2login-opensamlauthenticationprovider-decryption]] -==== Customizing Decryption +=== Customizing Decryption Spring Security decrypts ``, ``, and `` elements automatically by using the decryption <> registered in the <>. @@ -1451,7 +1449,7 @@ Trying to decrypt a signed assertion's elements before signature verification ma 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 +=== 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. @@ -1500,7 +1498,7 @@ open class SecurityConfig : WebSecurityConfigurerAdapter() { ==== [[servlet-saml2login-authenticatedprincipal]] -=== Using `Saml2AuthenticatedPrincipal` +== 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`. @@ -1540,356 +1538,3 @@ class MainController { [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 `` 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"] ----- -DefaultRelyingPartyRegistrationResolver 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 = - DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository) -val filter = Saml2MetadataFilter( - relyingPartyRegistrationResolver, - OpenSamlMetadataResolver() -) - -http { - //... - saml2Login { } - addFilterBefore(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")) ----- -==== - -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 ships with support for RP- and AP-initiated SAML 2.0 Single Logout. - -Briefly, there are two use cases Spring Security supports: - -* **RP-Initiated** - Your application has an endpoint that, when POSTed to, will logout the user and send a `saml2:LogoutRequest` to the asserting party. -Thereafter, the asserting party will send back a `saml2:LogoutResponse` and allow your application to respond -* **AP-Initiated** - Your application has an endpoint that will receive a `saml2:LogoutRequest` from the asserting party. -Your application will complete its logout at that point and then send a `saml2:LogoutResponse` to the asserting party. - -[NOTE] -In the **AP-Initiated** scenario, any local redirection that your application would do post-logout is rendered moot. -Once your application sends a `saml2:LogoutResponse`, it no longer has control of the browser. - -=== Minimal Configuration for Single Logout - -To use Spring Security's SAML 2.0 Single Logout feature, you will need the following things: - -* First, the asserting party must support SAML 2.0 Single Logout -* Second, the asserting party should be configured to sign and POST `saml2:LogoutRequest` s and `saml2:LogoutResponse` s your application's `/logout/saml2/slo` endpoint -* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s - -You can begin from the initial minimal example and add the following configuration: - -[source,java] ----- -@Value("${private.key}") RSAPrivateKey key; -@Value("${public.certificate}") X509Certificate certificate; - -@Bean -RelyingPartyRegistrationRepository registrations() { - Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate); - RelyingPartyRegistration registration = RelyingPartyRegistrations - .fromMetadataLocation("https://ap.example.org/metadata") - .registrationId("id") - .signingX509Credentials((signing) -> signing.add(credential)) <1> - .build(); - return new InMemoryRelyingPartyRegistrationRepository(registration); -} - -@Bean -SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception { - http - .authorizeRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .saml2Login(withDefaults()) - .saml2Logout(withDefaults()); <2> - - return http.build(); -} ----- -<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <> -<2> - Second, indicate that your application wants to use SAML SLO to logout the end user - -==== Runtime Expectations - -Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO. -Your application will then do the following: - -1. Logout the user and invalidate the session -2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `` based on the <> associated with the currently logged-in user. -3. Send a redirect or post to the asserting party based on the <> -4. Deserialize, verify, and process the `` sent by the asserting party -5. Redirect to any configured successful logout endpoint - -Also, your application can participate in an AP-initiated logout when the asserting party sends a `` to `/logout/saml2/slo`: - -1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `` sent by the asserting party -2. Logout the user and invalidate the session -3. Create, sign, and serialize a `` based on the <> associated with the just logged-out user -4. Send a redirect or post to the asserting party based on the <> - -=== Configuring Logout Endpoints - -There are three behaviors that can be triggered by different endpoints: - -* RP-initiated logout, which allows an authenticated user to `POST` and trigger the logout process by sending the asserting party a `` -* AP-initiated logout, which allows an asserting party to send a `` to the application -* AP logout response, which allows an asserting party to send a `` in response to the RP-initiated `` - -The first is triggered by performing normal `POST /logout` when the principal is of type `Saml2AuthenticatedPrincipal`. - -The second is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLRequest` signed by the asserting party. - -The third is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLResponse` signed by the asserting party. - -Because the user is already logged in or the original Logout Request is known, the `registrationId` is already known. -For this reason, `+{registrationId}+` is not part of these URLs by default. - -This URL is customizable in the DSL. - -For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`. -To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so: - -==== -.Java -[source,java,role="primary"] ----- -http - .saml2Logout((saml2) -> saml2 - .logoutRequest((request) -> request.logoutUrl("/SLOService.saml2")) - .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2")) - ); ----- -==== - -You should also configure these endpoints in your `RelyingPartyRegistration`. - -=== Customizing `` Resolution - -It's common to need to set other values in the `` than the defaults that Spring Security provides. - -By default, Spring Security will issue a `` and supply: - -* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation` -* The `ID` attribute - a GUID -* The `` element - from `RelyingPartyRegistration#getEntityId` -* The `` element - from `Authentication#getName` - -To add other values, you can use delegation, like so: - -[source,java] ----- -@Bean -Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationResolver registrationResolver) { - OpenSaml4LogoutRequestResolver logoutRequestResolver - new OpenSaml4LogoutRequestResolver(registrationResolver); - logoutRequestResolver.setParametersConsumer((parameters) -> { - String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute"); - String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"; - LogoutRequest logoutRequest = parameters.getLogoutRequest(); - NameID nameId = logoutRequest.getNameID(); - nameId.setValue(name); - nameId.setFormat(format); - }); - return logoutRequestResolver; -} ----- - -Then, you can supply your custom `Saml2LogoutRequestResolver` in the DSL as follows: - -[source,java] ----- -http - .saml2Logout((saml2) -> saml2 - .logoutRequest((request) -> request - .logoutRequestResolver(this.logoutRequestResolver) - ) - ); ----- - -=== Customizing `` Resolution - -It's common to need to set other values in the `` than the defaults that Spring Security provides. - -By default, Spring Security will issue a `` and supply: - -* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation` -* The `ID` attribute - a GUID -* The `` element - from `RelyingPartyRegistration#getEntityId` -* The `` element - `SUCCESS` - -To add other values, you can use delegation, like so: - -[source,java] ----- -@Bean -public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationResolver registrationResolver) { - OpenSaml4LogoutResponseResolver logoutRequestResolver = - new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver); - logoutRequestResolver.setParametersConsumer((parameters) -> { - if (checkOtherPrevailingConditions(parameters.getRequest())) { - parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT); - } - }); - return logoutRequestResolver; -} ----- - -Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows: - -[source,java] ----- -http - .saml2Logout((saml2) -> saml2 - .logoutRequest((request) -> request - .logoutRequestResolver(this.logoutRequestResolver) - ) - ); ----- - -=== Customizing `` Authentication - -To customize validation, you can implement your own `Saml2LogoutRequestValidator`. -At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutRequestValidator` like so: - -[source,java] ----- -@Component -public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator { - private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator(); - - @Override - public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) { - // verify signature, issuer, destination, and principal name - Saml2LogoutValidatorResult result = delegate.authenticate(authentication); - - LogoutRequest logoutRequest = // ... parse using OpenSAML - // perform custom validation - } -} ----- - -Then, you can supply your custom `Saml2LogoutRequestValidator` in the DSL as follows: - -[source,java] ----- -http - .saml2Logout((saml2) -> saml2 - .logoutRequest((request) -> request - .logoutRequestAuthenticator(myOpenSamlLogoutRequestAuthenticator) - ) - ); ----- - -=== Customizing `` Authentication - -To customize validation, you can implement your own `Saml2LogoutResponseValidator`. -At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutResponseValidator` like so: - -[source,java] ----- -@Component -public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator { - private final Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator(); - - @Override - public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) { - // verify signature, issuer, destination, and status - Saml2LogoutValidatorResult result = delegate.authenticate(parameters); - - LogoutResponse logoutResponse = // ... parse using OpenSAML - // perform custom validation - } -} ----- - -Then, you can supply your custom `Saml2LogoutResponseValidator` in the DSL as follows: - -[source,java] ----- -http - .saml2Logout((saml2) -> saml2 - .logoutResponse((response) -> response - .logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator) - ) - ); ----- - -=== Customizing `` storage - -When your application sends a ``, the value is stored in the session so that the `RelayState` parameter and the `InResponseTo` attribute in the `` can be verified. - -If you want to store logout requests in some place other than the session, you can supply your custom implementation in the DSL, like so: - -[source,java] ----- -http - .saml2Logout((saml2) -> saml2 - .logoutRequest((request) -> request - .logoutRequestRepository(myCustomLogoutRequestRepository) - ) - ); ----- diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc new file mode 100644 index 0000000000..1de18fa31f --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -0,0 +1,277 @@ +[[servlet-saml2login-logout]] += Performing Single Logout + +Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout. + +Briefly, there are two use cases Spring Security supports: + +* **RP-Initiated** - Your application has an endpoint that, when POSTed to, will logout the user and send a `saml2:LogoutRequest` to the asserting party. +Thereafter, the asserting party will send back a `saml2:LogoutResponse` and allow your application to respond +* **AP-Initiated** - Your application has an endpoint that will receive a `saml2:LogoutRequest` from the asserting party. +Your application will complete its logout at that point and then send a `saml2:LogoutResponse` to the asserting party. + +[NOTE] +In the **AP-Initiated** scenario, any local redirection that your application would do post-logout is rendered moot. +Once your application sends a `saml2:LogoutResponse`, it no longer has control of the browser. + +== Minimal Configuration for Single Logout + +To use Spring Security's SAML 2.0 Single Logout feature, you will need the following things: + +* First, the asserting party must support SAML 2.0 Single Logout +* Second, the asserting party should be configured to sign and POST `saml2:LogoutRequest` s and `saml2:LogoutResponse` s your application's `/logout/saml2/slo` endpoint +* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s + +You can begin from the initial minimal example and add the following configuration: + +[source,java] +---- +@Value("${private.key}") RSAPrivateKey key; +@Value("${public.certificate}") X509Certificate certificate; + +@Bean +RelyingPartyRegistrationRepository registrations() { + Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate); + RelyingPartyRegistration registration = RelyingPartyRegistrations + .fromMetadataLocation("https://ap.example.org/metadata") + .registrationId("id") + .signingX509Credentials((signing) -> signing.add(credential)) <1> + .build(); + return new InMemoryRelyingPartyRegistrationRepository(registration); +} + +@Bean +SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception { + http + .authorizeRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + .saml2Login(withDefaults()) + .saml2Logout(withDefaults()); <2> + + return http.build(); +} +---- +<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to xref:servlet/saml2/login.adoc#servlet-saml2login-rpr-duplicated[multiple instances] +<2> - Second, indicate that your application wants to use SAML SLO to logout the end user + +=== Runtime Expectations + +Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO. +Your application will then do the following: + +1. Logout the user and invalidate the session +2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `` based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the currently logged-in user. +3. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] +4. Deserialize, verify, and process the `` sent by the asserting party +5. Redirect to any configured successful logout endpoint + +Also, your application can participate in an AP-initiated logout when the asserting party sends a `` to `/logout/saml2/slo`: + +1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `` sent by the asserting party +2. Logout the user and invalidate the session +3. Create, sign, and serialize a `` based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the just logged-out user +4. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] + +== Configuring Logout Endpoints + +There are three behaviors that can be triggered by different endpoints: + +* RP-initiated logout, which allows an authenticated user to `POST` and trigger the logout process by sending the asserting party a `` +* AP-initiated logout, which allows an asserting party to send a `` to the application +* AP logout response, which allows an asserting party to send a `` in response to the RP-initiated `` + +The first is triggered by performing normal `POST /logout` when the principal is of type `Saml2AuthenticatedPrincipal`. + +The second is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLRequest` signed by the asserting party. + +The third is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLResponse` signed by the asserting party. + +Because the user is already logged in or the original Logout Request is known, the `registrationId` is already known. +For this reason, `+{registrationId}+` is not part of these URLs by default. + +This URL is customizable in the DSL. + +For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`. +To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so: + +==== +.Java +[source,java,role="primary"] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request.logoutUrl("/SLOService.saml2")) + .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2")) + ); +---- +==== + +You should also configure these endpoints in your `RelyingPartyRegistration`. + +== Customizing `` Resolution + +It's common to need to set other values in the `` than the defaults that Spring Security provides. + +By default, Spring Security will issue a `` and supply: + +* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation` +* The `ID` attribute - a GUID +* The `` element - from `RelyingPartyRegistration#getEntityId` +* The `` element - from `Authentication#getName` + +To add other values, you can use delegation, like so: + +[source,java] +---- +@Bean +Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationResolver registrationResolver) { + OpenSaml4LogoutRequestResolver logoutRequestResolver + new OpenSaml4LogoutRequestResolver(registrationResolver); + logoutRequestResolver.setParametersConsumer((parameters) -> { + String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute"); + String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"; + LogoutRequest logoutRequest = parameters.getLogoutRequest(); + NameID nameId = logoutRequest.getNameID(); + nameId.setValue(name); + nameId.setFormat(format); + }); + return logoutRequestResolver; +} +---- + +Then, you can supply your custom `Saml2LogoutRequestResolver` in the DSL as follows: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .logoutRequestResolver(this.logoutRequestResolver) + ) + ); +---- + +== Customizing `` Resolution + +It's common to need to set other values in the `` than the defaults that Spring Security provides. + +By default, Spring Security will issue a `` and supply: + +* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation` +* The `ID` attribute - a GUID +* The `` element - from `RelyingPartyRegistration#getEntityId` +* The `` element - `SUCCESS` + +To add other values, you can use delegation, like so: + +[source,java] +---- +@Bean +public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationResolver registrationResolver) { + OpenSaml4LogoutResponseResolver logoutRequestResolver = + new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver); + logoutRequestResolver.setParametersConsumer((parameters) -> { + if (checkOtherPrevailingConditions(parameters.getRequest())) { + parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT); + } + }); + return logoutRequestResolver; +} +---- + +Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .logoutRequestResolver(this.logoutRequestResolver) + ) + ); +---- + +== Customizing `` Authentication + +To customize validation, you can implement your own `Saml2LogoutRequestValidator`. +At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutRequestValidator` like so: + +[source,java] +---- +@Component +public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator { + private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator(); + + @Override + public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) { + // verify signature, issuer, destination, and principal name + Saml2LogoutValidatorResult result = delegate.authenticate(authentication); + + LogoutRequest logoutRequest = // ... parse using OpenSAML + // perform custom validation + } +} +---- + +Then, you can supply your custom `Saml2LogoutRequestValidator` in the DSL as follows: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .logoutRequestAuthenticator(myOpenSamlLogoutRequestAuthenticator) + ) + ); +---- + +== Customizing `` Authentication + +To customize validation, you can implement your own `Saml2LogoutResponseValidator`. +At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutResponseValidator` like so: + +[source,java] +---- +@Component +public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator { + private final Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator(); + + @Override + public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) { + // verify signature, issuer, destination, and status + Saml2LogoutValidatorResult result = delegate.authenticate(parameters); + + LogoutResponse logoutResponse = // ... parse using OpenSAML + // perform custom validation + } +} +---- + +Then, you can supply your custom `Saml2LogoutResponseValidator` in the DSL as follows: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutResponse((response) -> response + .logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator) + ) + ); +---- + +== Customizing `` storage + +When your application sends a ``, the value is stored in the session so that the `RelayState` parameter and the `InResponseTo` attribute in the `` can be verified. + +If you want to store logout requests in some place other than the session, you can supply your custom implementation in the DSL, like so: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .logoutRequestRepository(myCustomLogoutRequestRepository) + ) + ); +---- diff --git a/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc new file mode 100644 index 0000000000..f63e93ba60 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc @@ -0,0 +1,74 @@ +[[servlet-saml2login-metadata]] += Producing `` 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"] +---- +DefaultRelyingPartyRegistrationResolver 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 = + DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository) +val filter = Saml2MetadataFilter( + relyingPartyRegistrationResolver, + OpenSamlMetadataResolver() +) + +http { + //... + saml2Login { } + addFilterBefore(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")) +---- +==== + +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")) +---- +====