|
@@ -1,293 +1,572 @@
|
|
-[[servlet-saml2-login]]
|
|
|
|
|
|
+[[servlet-saml2login]]
|
|
== SAML 2.0 Login
|
|
== SAML 2.0 Login
|
|
|
|
+:figures: images/servlet/saml2
|
|
|
|
+:icondir: images/icons
|
|
|
|
|
|
-The SAML 2.0 Login, `saml2Login()`, feature provides an application with the capability to have users log in to the application by using their existing account at an SAML 2.0 Identity Provider (Okta, ADFS, etc).
|
|
|
|
|
|
+The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Service Provider, having users log in to the application by using their existing account at a SAML 2.0 Identity Provider (Okta, ADFS, etc).
|
|
|
|
|
|
NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in
|
|
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].
|
|
https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles].
|
|
-Our implementation is currently limited to a simple authentication scheme.
|
|
|
|
|
|
|
|
[[servlet-saml2-spring-security-history]]
|
|
[[servlet-saml2-spring-security-history]]
|
|
-=== SAML 2 Support in Spring Security
|
|
|
|
-
|
|
|
|
-SAML 2 Service Provider, SP a.k.a. a relying party, support existed as an
|
|
|
|
-https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[independent project]
|
|
|
|
-since 2009. The 1.0.x branch is still in use, including in the
|
|
|
|
-https://github.com/cloudfoundry/uaa[Cloud Foundry User Account and Authentication Server] that
|
|
|
|
-also created a SAML 2.0 Identity Provider implementation based on the SP implementation.
|
|
|
|
-
|
|
|
|
-In 2018 we experimented with creating an updated implementation of both a
|
|
|
|
-https://github.com/spring-projects/spring-security-saml#spring-saml[Service Provider and Identity Provider]
|
|
|
|
-as a standalone library. After careful, and lengthy, deliberation we, the Spring Security team, decided
|
|
|
|
-to discontinue that effort. While this effort created a replacement for that standalone 1.0.x library
|
|
|
|
-we didn't feel that we should build a library on top of another library.
|
|
|
|
-
|
|
|
|
-Instead we opted to provide framework support for SAML 2 authentication as part of
|
|
|
|
-https://github.com/spring-projects/spring-security[core Spring Security] instead.
|
|
|
|
-
|
|
|
|
-[[servlet-saml2-login-concepts]]
|
|
|
|
-=== Saml 2 Login - High Level Concepts
|
|
|
|
-
|
|
|
|
-`saml2Login()` is aimed to support a fraction of the https://saml.xml.org/saml-specifications[SAML 2 feature set]
|
|
|
|
-with a focus on authentication being a Service Provider, SP, a relying party, receiving XML assertions from an
|
|
|
|
-Identity Provider, aka an asserting party.
|
|
|
|
-
|
|
|
|
-A SAML 2 login, or authentication, is the concept that the SP receives and validates an XML message called
|
|
|
|
-an assertion from an IDP.
|
|
|
|
-
|
|
|
|
-There are currently two supported authentication flows
|
|
|
|
-
|
|
|
|
-1. IDP Initiated flow - example: You login in directly to Okta, and then select a web application to be authenticated for.
|
|
|
|
-Okta, the IDP, sends an assertion to the web application, the SP.
|
|
|
|
-2. SP Initiated flow - example: You access a web application, a SP, the application sends an
|
|
|
|
-authentication request to the IDP requesting an assertion. Upon successful authentication on the IDP,
|
|
|
|
-the IDP sends an assertion to the SP.
|
|
|
|
-
|
|
|
|
-[[servlet-saml2-login-feature-set]]
|
|
|
|
-=== Saml 2 Login - Current Feature Set
|
|
|
|
-
|
|
|
|
-1. Service Provider (SP/Relying Party) is identified by `+entityId = {baseUrl}/saml2/service-provider-metadata/{registrationId}+`
|
|
|
|
-2. Receive assertion embedded in a SAML response via Http-POST or Http-Redirect at `+{baseUrl}/login/saml2/sso/{registrationId}+`
|
|
|
|
-3. Requires the assertion to be signed, unless the response is signed
|
|
|
|
-4. Supports encrypted assertions
|
|
|
|
-5. Supports encrypted NameId elements
|
|
|
|
-6. Allows for extraction of assertion attributes into authorities by using a `Converter<Assertion, Collection<? extends GrantedAuthority>>`
|
|
|
|
-7. Allows mapping and approved access listing for authorities by using a `GrantedAuthoritiesMapper`
|
|
|
|
-8. Public keys in `java.security.cert.X509Certificate` format.
|
|
|
|
-9. SP Initiated Authentication via an `AuthNRequest`
|
|
|
|
-
|
|
|
|
-[[servlet-saml2-login-tbd]]
|
|
|
|
-==== Saml 2 Login - Not Yet Supported
|
|
|
|
-
|
|
|
|
-1. Mappings assertion conditions and attributes to session features (timeout, tracking, etc)
|
|
|
|
-2. Single logout
|
|
|
|
-3. Receiving and validating standalone assertion (not wrapped in a response object)
|
|
|
|
-
|
|
|
|
-[[servlet-saml2-javaconfig]]
|
|
|
|
-=== Saml 2 Login - Introduction to Java Configuration
|
|
|
|
-
|
|
|
|
-To add `saml2Login()` to a Spring Security filter chain,
|
|
|
|
-the minimal Java configuration requires a configuration repository,
|
|
|
|
-the `RelyingPartyRegistrationRepository`, that contains the SAML configuration and
|
|
|
|
-the invocation of the `HttpSecurity.saml2Login()` method:
|
|
|
|
-[source,java]
|
|
|
|
|
|
+Since 2009, support for service providers has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project].
|
|
|
|
+In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper.
|
|
|
|
+This process is similar to the once started in 2017 for <<oauth2,Spring Security's OAuth 2.0 support>>.
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+====
|
|
|
|
+A working sample for {gh-samples-url}/boot/saml2login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security repository].
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-minimaldependencies]]
|
|
|
|
+=== Minimal Dependencies
|
|
|
|
+
|
|
|
|
+SAML 2.0 service provider support is found in `spring-security-saml2-service-provider`.
|
|
|
|
+It builds off of the OpenSAML library.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-minimalconfiguration]]
|
|
|
|
+=== Minimal Configuration
|
|
|
|
+
|
|
|
|
+When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a service provider consists of two basic steps.
|
|
|
|
+First, include the needed dependencies and second, indicate the necessary identity provider metadata.
|
|
|
|
+
|
|
|
|
+==== Specifying Identity Provider Metadata
|
|
|
|
+
|
|
|
|
+In a Spring Boot application, to specify an identity provider's metadata, simply do:
|
|
|
|
+
|
|
|
|
+[source,yml]
|
|
----
|
|
----
|
|
-@EnableWebSecurity
|
|
|
|
-public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
|
|
|
+spring:
|
|
|
|
+ security:
|
|
|
|
+ saml2:
|
|
|
|
+ relyingparty:
|
|
|
|
+ registration:
|
|
|
|
+ example:
|
|
|
|
+ identityprovider:
|
|
|
|
+ entity-id: https://idp.example.com/issuer
|
|
|
|
+ verification.credentials:
|
|
|
|
+ - certificate-location: "classpath:idp.crt"
|
|
|
|
+ singlesignon.url: https://idp.example.com/issuer/SSO.saml2
|
|
|
|
+ singlesignon.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.saml2` is the endpoint where the identity provider is expecting `AuthnRequest` s.
|
|
|
|
+
|
|
|
|
+And that's it!
|
|
|
|
+
|
|
|
|
+From here, consider jumping to:
|
|
|
|
+
|
|
|
|
+* <<servlet-saml2login-architecture,How SAML 2.0 Login Works>>
|
|
|
|
+* <<servlet-saml2login-authenticatedprincipal,How to Use the `Saml2AuthenticatedPrincipal`>>
|
|
|
|
+* <<servlet-saml2login-sansboot,How to Configure without Spring Boot>>
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-architecture]]
|
|
|
|
+=== How SAML 2.0 Login Works
|
|
|
|
+
|
|
|
|
+When the above configuration is used, the application will automatically configure itself as a SAML 2.0 Service Provider - also called a relying party - that points to one or many identity providers - also called asserting parties.
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party.
|
|
|
|
+
|
|
|
|
+There are two supported authentication flows:
|
|
|
|
+
|
|
|
|
+1. AP-Initiated flow, which is when you login in directly to the asserting party, and then select a web application to be authenticated for.
|
|
|
|
+2. RP-Initiated flow, which is when you access your relying party, and it sends an authentication request to the asserting party.
|
|
|
|
+You then authenticate and are redirected back to the relying party.
|
|
|
|
+
|
|
|
|
+To see this in action, you can navigate to a protected page in your app, for example `http://localhost:8080` is protected by default, and it will redirect you to the configured asserting party where you can authenticate.
|
|
|
|
+
|
|
|
|
+Once authenticated, the asserting party will issue an authentication response.
|
|
|
|
+Your application will then:
|
|
|
|
|
|
- @Bean
|
|
|
|
- public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
|
|
|
|
- //SAML configuration
|
|
|
|
- //Mapping this application to one or more Identity Providers
|
|
|
|
- return new InMemoryRelyingPartyRegistrationRepository(...);
|
|
|
|
|
|
+1. Validate the response's signature against the set of public keys obtained from configuration
|
|
|
|
+2. Decrypt any encrypted assertions
|
|
|
|
+3. Validate the `ExpiresAt` and `NotBefore` timestamps, the `Issuer` url, the `<Subject>` and any `<AudienceRestriction>` conditions
|
|
|
|
+4. Map each attribute in the `AttributeStatement` into principal attributes
|
|
|
|
+5. Grant the authority `ROLE_USER` to the resulting authentication
|
|
|
|
+
|
|
|
|
+[TIP]
|
|
|
|
+Because Spring Security iterates through the set of configured public keys, it's possible to achieve key rotation by adding a new key to the list before removing a key you are retiring.
|
|
|
|
+
|
|
|
|
+The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Saml2AuthenticatedPrincipal` object, and `Authentication#getName` maps to the first assertion's `NameID` element, if one is present.
|
|
|
|
+
|
|
|
|
+[[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 { }
|
|
}
|
|
}
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
|
|
- @Override
|
|
|
|
- protected void configure(HttpSecurity http) throws Exception {
|
|
|
|
|
|
+If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one.
|
|
|
|
+
|
|
|
|
+Replacing this is as simple as exposing the bean within the application:
|
|
|
|
+
|
|
|
|
+.Custom SAML 2.0 Login Configuration
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
|
|
|
+ protected void configure(HttpSecurity http) {
|
|
http
|
|
http
|
|
.authorizeRequests(authorize -> authorize
|
|
.authorizeRequests(authorize -> authorize
|
|
|
|
+ .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
|
|
.anyRequest().authenticated()
|
|
.anyRequest().authenticated()
|
|
)
|
|
)
|
|
- .saml2Login(withDefaults())
|
|
|
|
- ;
|
|
|
|
|
|
+ .saml2Login(withDefaults());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
----
|
|
----
|
|
|
|
|
|
-The bean declaration is a convenient, but optional, approach.
|
|
|
|
-You can directly wire up the repository using a method call
|
|
|
|
-[source,java]
|
|
|
|
|
|
+.Kotlin
|
|
|
|
+[source,kotlin,role="secondary"]
|
|
----
|
|
----
|
|
@EnableWebSecurity
|
|
@EnableWebSecurity
|
|
-public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
|
|
|
+class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
|
|
|
|
+ override fun configure(http: HttpSecurity) {
|
|
|
|
+ http {
|
|
|
|
+ authorizeRequests {
|
|
|
|
+ authorize("/messages/**", hasAuthority("ROLE_USER"))
|
|
|
|
+ authorize(anyRequest, authenticated)
|
|
|
|
+ }
|
|
|
|
+ saml2Login {
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
|
|
- @Override
|
|
|
|
- protected void configure(HttpSecurity http) throws Exception {
|
|
|
|
|
|
+The above requires the role of `USER` for any URL that starts with `/messages/`.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2login-relyingpartyregistrationrepository]]
|
|
|
|
+The second `@Bean` Spring Boot creates is a `RelyingPartyRegistrationRepository`, which represents the AP and RP metadata.
|
|
|
|
+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
|
|
|
|
+====
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+@Value("${metadata.location}")
|
|
|
|
+String assertingPartyMetadataLocation;
|
|
|
|
+
|
|
|
|
+@Bean
|
|
|
|
+public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
|
|
|
|
+ RelyingPartyRegistration registration = RelyingPartyRegistrations
|
|
|
|
+ .fromMetadataLocation(assertingPartyMetadataLocation)
|
|
|
|
+ .registrationId("example")
|
|
|
|
+ .build();
|
|
|
|
+ return new InMemoryRelyingPartyRegistrationRepository(registration);
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+Or you can provide each detail manually, as you can see below:
|
|
|
|
+
|
|
|
|
+.Relying Party Registration Repository Manual Configuration
|
|
|
|
+====
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+@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);
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+====
|
|
|
|
+
|
|
|
|
+[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-configuration:
|
|
|
|
+
|
|
|
|
+.Custom Relying Party Registration DSL
|
|
|
|
+====
|
|
|
|
+.Java
|
|
|
|
+[source,java,role="primary"]
|
|
|
|
+----
|
|
|
|
+@EnableWebSecurity
|
|
|
|
+public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
|
|
|
+ protected void configure(HttpSecurity http) {
|
|
http
|
|
http
|
|
.authorizeRequests(authorize -> authorize
|
|
.authorizeRequests(authorize -> authorize
|
|
|
|
+ .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
|
|
.anyRequest().authenticated()
|
|
.anyRequest().authenticated()
|
|
)
|
|
)
|
|
.saml2Login(saml2 -> saml2
|
|
.saml2Login(saml2 -> saml2
|
|
- .relyingPartyRegistrationRepository(...)
|
|
|
|
- )
|
|
|
|
- ;
|
|
|
|
|
|
+ .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-saml2-relyingpartyregistration]]
|
|
[[servlet-saml2-relyingpartyregistration]]
|
|
-==== RelyingPartyRegistration
|
|
|
|
-The https://github.com/spring-projects/spring-security/blob/5.2.0.RELEASE/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java[`RelyingPartyRegistration`]
|
|
|
|
-object represents the mapping between this application, the SP, and the asserting party, the IDP.
|
|
|
|
|
|
+=== RelyingPartyRegistration
|
|
|
|
+A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
|
|
|
|
+instance represents a link between an RP and AP's metadata.
|
|
|
|
+
|
|
|
|
+In a `RelyingPartyRegistration`, you can provide RP metadata like its `Issuer` value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads.
|
|
|
|
+
|
|
|
|
+Also, you can provide AP metadata like its `Issuer` value, where it expects AuthnRequests to be sent to, and any credentials that it owns for the purposes of the RP verifying or encrypting paylods.
|
|
|
|
+
|
|
|
|
+The following `RelyingPartyRegistration` is the minimum required for most setups:
|
|
|
|
+
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
|
|
|
|
+ .fromMetadataLocation("https://ap.example.org/metadata")
|
|
|
|
+ .registrationId("my-id")
|
|
|
|
+ .build();
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+Though a more sophisticated setup is also possible, like so:
|
|
|
|
+
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
|
|
|
|
+ .entityId("{baseUrl}/{registrationId}")
|
|
|
|
+ .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
|
|
|
|
+ .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
|
|
|
|
+ .assertingParty(party -> party
|
|
|
|
+ .entityId("https://ap.example.org")
|
|
|
|
+ .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
|
|
|
|
+ .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
|
|
|
|
+ );
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+[TIP]
|
|
|
|
+The top-level metadata methods are details about the RP. The methods inside `assertingPartyDetails` are details about the AP.
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+The location where an RP is expecting SAML Responses is known as the Assertion Consumer Service Location.
|
|
|
|
+
|
|
|
|
+The default for the RP's `entityId` is `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`.
|
|
|
|
+This is this value needed when configuring the AP to know about your RP.
|
|
|
|
+
|
|
|
|
+The default for the `assertionConsumerServiceLocation` is `+/login/saml2/sso/{registrationId}+`.
|
|
|
|
+It's mapped by default to {security-api-url}org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.html[`Saml2WebSsoAuthenticationFilter`] in the filter chain.
|
|
|
|
|
|
[[servlet-saml2-rpr-uripatterns]]
|
|
[[servlet-saml2-rpr-uripatterns]]
|
|
-===== URI Patterns
|
|
|
|
-
|
|
|
|
-URI patterns are frequenty used to automatically generate URIs based on
|
|
|
|
-an incoming request. The URI patterns in `saml2Login` can contain the following variables
|
|
|
|
-
|
|
|
|
-* `baseUrl`
|
|
|
|
-* `registrationId`
|
|
|
|
-* `baseScheme`
|
|
|
|
-* `baseHost`
|
|
|
|
-* `basePort`
|
|
|
|
-
|
|
|
|
-For example:
|
|
|
|
-`+{baseUrl}/login/saml2/sso/{registrationId}+`
|
|
|
|
-
|
|
|
|
-[[servlet-saml2-rpr-relyingparty]]
|
|
|
|
-===== Relying Party
|
|
|
|
-
|
|
|
|
-* `registrationId` - (required) a unique identifer for this configuration mapping.
|
|
|
|
-This identifier may be used in URI paths, so care should be taken that no URI encoding is required.
|
|
|
|
-* `localEntityIdTemplate` - (optional) A URI pattern that creates an entity ID for this application based on the incoming request. The default is
|
|
|
|
-`+{baseUrl}/saml2/service-provider-metadata/{registrationId}+` and for a small sample application
|
|
|
|
-it would look like
|
|
|
|
-```
|
|
|
|
-http://localhost:8080/saml2/service-provider-metadata/my-test-configuration
|
|
|
|
-```
|
|
|
|
-There is no requirement that this configuration option is a pattern, it can be a fixed URI value.
|
|
|
|
-
|
|
|
|
-* `assertionConsumerServiceUrlTemplate` - (optional) A URI pattern that denotes the assertion
|
|
|
|
-consumer service URI to be sent with any `AuthNRequest` from the SP to the IDP during the SP initiated flow.
|
|
|
|
-While this can be a pattern the actual URI must resolve to the ACS endpoint on the SP.
|
|
|
|
-The default value is `+{baseUrl}/login/saml2/sso/{registrationId}+` and maps directly to the
|
|
|
|
-https://github.com/spring-projects/spring-security/blob/5.2.0.RELEASE/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java#L42[`Saml2WebSsoAuthenticationFilter`] endpoint
|
|
|
|
-* `credentials` - A list of credentials, private keys and x509 certificates, used for
|
|
|
|
-message signing, verification, encryption and decryption.
|
|
|
|
-This list can contain redundant credentials to allow for easy rotation of credentials.
|
|
|
|
-For example
|
|
|
|
-** [0] - X509Certificate{VERIFICATION,ENCRYPTION} - The IDP's first public key used for
|
|
|
|
-verification and encryption.
|
|
|
|
-** [1] - X509Certificate/{VERIFICATION,ENCRYPTION} - The IDP's second verification key used for verification.
|
|
|
|
-Encryption is always done using the first `ENCRYPTION` key in the list.
|
|
|
|
-** [2] - PrivateKey/X509Certificate{SIGNING,DECRYPTION} - The SP's first signing and decryption credential.
|
|
|
|
-** [3] - PrivateKey/X509Certificate{SIGNING,DECRYPTION} - The SP's second decryption credential.
|
|
|
|
-Signing is always done using the first `SIGNING` key in the list.
|
|
|
|
-* `ProviderDetails#entityId` - (required) the entity ID of the Identity Provider. Always a fixed URI value or string,
|
|
|
|
-no patterns allowed.
|
|
|
|
-* `ProviderDetails#webSsoUrl` - (required) a fixed URI value for the IDP Single Sign On endpoint where
|
|
|
|
-the SP sends the `AuthNRequest` messages.
|
|
|
|
-* `ProviderDetails#signAuthNRequest` - A boolean indicating whether or not to sign the `AuthNRequest` with the SP's private key, defaults to `true`
|
|
|
|
-* `ProviderDetails#binding` - A `Saml2MessageBinding` indicating what kind of binding to use for the `AuthNRequest`, whether that be `REDIRECT` or `POST`, defaults to `REDIRECT`
|
|
|
|
-
|
|
|
|
-When an incoming message is received, signatures are always required, the system will first attempt
|
|
|
|
-to validate the signature using the certificate at index [0] and only move to the second
|
|
|
|
-credential if the first one fails.
|
|
|
|
-
|
|
|
|
-In a similar fashion, the SP configured private keys are used for decryption and attempted in the same order.
|
|
|
|
-The first SP credential (`type=SIGNING`) will be used when messages to the IDP are signed.
|
|
|
|
|
|
+==== URI Patterns
|
|
|
|
+
|
|
|
|
+You probably noticed in the above examples the `+{baseUrl}+` and `+{registrationId}+` placeholders.
|
|
|
|
+
|
|
|
|
+These are useful for generating URIs. As such, the RP's `entityId` and `assertionConsumerServiceLocation` support the following placeholders:
|
|
|
|
+
|
|
|
|
+* `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/my-id+`
|
|
|
|
+
|
|
|
|
+The `entityId` above was defined as:
|
|
|
|
+
|
|
|
|
+`+{baseUrl}/{registrationId}+`
|
|
|
|
+
|
|
|
|
+which in a deployed application would translate to
|
|
|
|
+
|
|
|
|
+`+https://rp.example.org/my-id+`
|
|
|
|
+
|
|
|
|
+[[servlet-saml2-rpr-credentials]]
|
|
|
|
+==== Credentials
|
|
|
|
+
|
|
|
|
+You also likely noticed the credential that was used.
|
|
|
|
+
|
|
|
|
+Oftentimes, an RP will use the same key to sign payloads as well as decrypt them.
|
|
|
|
+Or it will use the same key to verify payloads as well as encrypt them.
|
|
|
|
+
|
|
|
|
+Because of this, Spring Security ships with `Saml2X509Credential`, a SAML-specific credential that simplifies configuring the same key for different use cases.
|
|
|
|
+
|
|
|
|
+At a minimum, it's necessary to have a certificate from the Asserting Party so that the AP's signed responses can be verified.
|
|
|
|
+
|
|
|
|
+To construct a `Saml2X509Credential` that you'll use to verify assertions from the Asserting Party, you can load the file and use `CertificateFactory` like so:
|
|
|
|
+
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+Resource resource = new ClassPathResource("ap.crt");
|
|
|
|
+try (InputStream is = resource.getInputStream()) {
|
|
|
|
+ X509Certificate certificate = (X509Certificate)
|
|
|
|
+ CertificateFactory.getInstance("X.509").generateCertificate(is);
|
|
|
|
+ return Saml2X509Credential.verification(certificate);
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+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:
|
|
|
|
+
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+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);
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+[TIP]
|
|
|
|
+When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
|
|
|
|
|
|
[[servlet-saml2-rpr-duplicated]]
|
|
[[servlet-saml2-rpr-duplicated]]
|
|
-===== Duplicated Relying Party Configurations
|
|
|
|
|
|
+==== Duplicated Relying Party Configurations
|
|
|
|
+
|
|
|
|
+When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances:
|
|
|
|
+
|
|
|
|
+* The RP's `entityId`
|
|
|
|
+* Its `assertionConsumerServiceLocation`, and
|
|
|
|
+* Its credentials, for example its signing or decryption credentials
|
|
|
|
|
|
-In the use case where an application uses multiple identity providers it becomes
|
|
|
|
-obvious that some configuration is duplicated between two `RelyingPartyRegistration` objects
|
|
|
|
|
|
+What's nice about this setup is credentials may be more easily rotated for some identity providers vs others.
|
|
|
|
|
|
-* localEntityIdTemplate
|
|
|
|
-* credentials (all SP credentials, IDP credentials change)
|
|
|
|
-* assertionConsumerServiceUrlTemplate
|
|
|
|
|
|
+The duplication can be alleviated in a few different ways.
|
|
|
|
|
|
-While there is some drawback in duplicating configuration values the back end
|
|
|
|
-configuration repository does not need to replicate this data storage model.
|
|
|
|
|
|
+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: ...
|
|
|
|
+----
|
|
|
|
|
|
-There is a benefit that comes with this setup. Credentials may be more easily rotated
|
|
|
|
-for some identity providers vs others. This object model can ensure that there is no
|
|
|
|
-disruption when configuration is changed in a multi IDP use case and you're not able to rotate
|
|
|
|
-credentials on all the identity providers.
|
|
|
|
|
|
+Second, in a database, it's not necessary to replicate `RelyingPartyRegistration`'s model.
|
|
|
|
|
|
-[[servlet-saml2-serviceprovider-metadata]]
|
|
|
|
-==== Service Provider Metadata
|
|
|
|
|
|
+Third, in Java, you can create a custom configuration method, like so:
|
|
|
|
|
|
-The Spring Security SAML 2 implementation does provide an endpoint for downloading
|
|
|
|
-SP metadata in XML format. The provider is mapped to: `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`
|
|
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+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(
|
|
|
|
+ RelyingPartyRegistration
|
|
|
|
+ .fromMetadataLocation(oktaMetadataUrl)
|
|
|
|
+ .registrationId("okta")).build();
|
|
|
|
+
|
|
|
|
+ RelyingPartyRegistration azure = addRelyingPartyDetails(
|
|
|
|
+ RelyingPartyRegistration
|
|
|
|
+ .fromMetadataLocation(oktaMetadataUrl)
|
|
|
|
+ .registrationId("azure")).build();
|
|
|
|
+
|
|
|
|
+ return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
|
|
[[servlet-saml2-sp-initiated]]
|
|
[[servlet-saml2-sp-initiated]]
|
|
-==== Authentication Requests - SP Initiated Flow
|
|
|
|
|
|
+=== RP-initiated Login
|
|
|
|
|
|
-To initiate an authentication from the web application, you can redirect to:
|
|
|
|
|
|
+You can initiate login by navigating to the SAML 2.0 login endpoint for a given `RelyingPartyRegistration`, like so:
|
|
|
|
|
|
`+{baseUrl}/saml2/authenticate/{registrationId}+`
|
|
`+{baseUrl}/saml2/authenticate/{registrationId}+`
|
|
|
|
|
|
-This endpoint will generate an `AuthNRequest` either as a Redirect or POST depending on your `RelyingPartyRegistration`.
|
|
|
|
|
|
+Where you replace `+{baseUrl}+` and `+{registrationId}+` with the deployed location of your application and the identifier for that registration.
|
|
|
|
|
|
-[[servlet-saml2-sp-initiated-factory]]
|
|
|
|
-==== Customizing the AuthNRequest
|
|
|
|
|
|
+For example, if you were deployed to `https://rp.example.org` and you gave your registration an ID of `ping`, you could navigate to:
|
|
|
|
|
|
-To adjust the `AuthNRequest`, you can publish an instance of `Saml2AuthenticationRequestFactory`.
|
|
|
|
|
|
+`https://rp.example.org/saml2/authenticate/ping`
|
|
|
|
|
|
-For example, if you wanted to configure the `AuthNRequest` to request the IDP to send the SAML `Assertion` by REDIRECT, you could do:
|
|
|
|
|
|
+You can modify this by post-processing the `Saml2WebSsoAuthenticationRequestFilter`.
|
|
|
|
+
|
|
|
|
+By default, this filter will use HTTP-Redirect for the AuthNRequest.
|
|
|
|
+However, by setting `RelyingPartyRegistration.AssertingPartyDetails#singleSignOnServiceBinding` to `Saml2MessageType.POST`, like so, it will use HTTP-POST instead:
|
|
|
|
|
|
[source,java]
|
|
[source,java]
|
|
----
|
|
----
|
|
-@Bean
|
|
|
|
-public Saml2AuthenticationRequestFactory authenticationRequestFactory() {
|
|
|
|
- OpenSamlAuthenticationRequestFactory authenticationRequestFactory =
|
|
|
|
- new OpenSamlAuthenticationRequestFactory();
|
|
|
|
- authenticationRequestFactory.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
|
|
|
|
- return authenticationRequestFactory;
|
|
|
|
-}
|
|
|
|
|
|
+RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
|
|
|
|
+ // ...
|
|
|
|
+ .assertingPartyDetails(party -> party
|
|
|
|
+ // ...
|
|
|
|
+ .singleSignOnServiceBinding(Saml2MessageType.POST)
|
|
|
|
+ );
|
|
----
|
|
----
|
|
|
|
|
|
-[[servlet-saml2-sp-initiated-factory-delegate]]
|
|
|
|
-==== Delegating to an AuthenticationRequestFactory
|
|
|
|
|
|
+[[servlet-saml2-sp-initiated-factory]]
|
|
|
|
+==== Customizing the AuthNRequest
|
|
|
|
+
|
|
|
|
+There are a number of reasons that you may want to adjust an AuthNRequest.
|
|
|
|
+For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
|
|
|
|
|
|
-Or, in circumstances where you need more control over what is sent as parameters to the `AuthenticationRequestFactory`, you can use delegation:
|
|
|
|
|
|
+If you don't need information from the `HttpServletRequest` to make your decision, then the easiest way is to register a custom `AuthnRequestMarshaller` with OpenSAML.
|
|
|
|
+This will give you access to post-process the `AuthnRequest` instance before it's serialized.
|
|
|
|
+
|
|
|
|
+But, if you do need something from the request, then you can use create a custom `Saml2AuthenticationRequestContext` implementation and then a `Converter<Saml2AuthenticationRequestContext, AuthnRequest>` to build it yourself, like so:
|
|
|
|
|
|
[source,java]
|
|
[source,java]
|
|
----
|
|
----
|
|
@Component
|
|
@Component
|
|
-public class IssuerSaml2AuthenticationRequestFactory implements Saml2AuthenticationRequestFactory {
|
|
|
|
- private OpenSamlAuthenticationRequestFactory delegate = new OpenSamlAuthenticationRequestFactory();
|
|
|
|
|
|
+public class AuthnRequestConverter implements
|
|
|
|
+ Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
|
|
|
|
|
|
- @Override
|
|
|
|
- public String createAuthenticationRequest(Saml2AuthenticationRequest request) {
|
|
|
|
- return this.delegate.createAuthenticationRequest(request);
|
|
|
|
- }
|
|
|
|
|
|
+ private final AuthnRequestBuilder authnRequestBuilder;
|
|
|
|
+ private final IssuerBuilder issuerBuilder;
|
|
|
|
|
|
- @Override
|
|
|
|
- public Saml2PostAuthenticationRequest createPostAuthenticationRequest
|
|
|
|
- (Saml2AuthenticationRequestContext context) {
|
|
|
|
|
|
+ // ... constructor
|
|
|
|
|
|
- String issuer = // ... calculate issuer
|
|
|
|
|
|
+ public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
|
|
|
|
+ MySaml2AuthenticationRequestContext custom = (MySaml2AuthenticationRequestContext) context;
|
|
|
|
+ Issuer issuer = issuerBuilder.buildObject();
|
|
|
|
+ issuer.setValue(context.getIssuer());
|
|
|
|
|
|
- Saml2AuthenticationRequestContext customIssuer = Saml2AuthenticationRequestContext.builder()
|
|
|
|
- .assertionConsumerServiceUrl(context.getAssertionConsumerServiceUrl())
|
|
|
|
- .issuer(issuer)
|
|
|
|
- .relayState(context.getRelayState())
|
|
|
|
- .relyingPartyRegistration(context.getRelyingPartyRegistration())
|
|
|
|
- .build();
|
|
|
|
|
|
+ AuthnRequest authnRequest = authnRequestBuilder.buildObject();
|
|
|
|
+ authnRequest.setIssuer(iss);
|
|
|
|
+ authnRequest.setDestination(context.getDestination());
|
|
|
|
+ authnRequest.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl());
|
|
|
|
|
|
- return this.delegate.createPostAuthenticationRequest(customIssuer);
|
|
|
|
|
|
+ // ... additional settings
|
|
|
|
+
|
|
|
|
+ authRequest.setForceAuthn(custom.getForceAuthn());
|
|
|
|
+ return authnRequest;
|
|
}
|
|
}
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
|
|
- @Override
|
|
|
|
- public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest
|
|
|
|
- (Saml2AuthenticationRequestContext context) {
|
|
|
|
|
|
+And then you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory`, and publish them as `@Bean` s:
|
|
|
|
|
|
- throw new UnsupportedOperationException("unsupported");
|
|
|
|
- }
|
|
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+@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) {
|
|
|
|
+
|
|
|
|
+ OpenSamlAuthenticationRequestFactory authenticationRequestFactory =
|
|
|
|
+ new OpenSamlAuthenticationRequestFactory();
|
|
|
|
+ authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
|
|
|
|
+ return authenticationRequestFactory;
|
|
}
|
|
}
|
|
----
|
|
----
|
|
|
|
|
|
[[servlet-saml2-login-customize]]
|
|
[[servlet-saml2-login-customize]]
|
|
=== Customizing Authentication Logic
|
|
=== Customizing Authentication Logic
|
|
|
|
|
|
-By default Spring Security configures the `OpenSamlAuthenticationProvider`
|
|
|
|
-to validate and parse the SAML 2 response and assertions that are received.
|
|
|
|
-This provider has three configuration options
|
|
|
|
|
|
+To verify SAML 2.0 Responses, Spring Security uses `OpenSamlAuthenticationProvider` by default.
|
|
|
|
|
|
-1. An authorities extractor - extract group information from the assertion
|
|
|
|
-2. An authorities mapper - map extracted group information to internal authorities
|
|
|
|
-3. Response time validation duration - the built in tolerances for timestamp validation
|
|
|
|
-should be used when there may be a time synchronization issue.
|
|
|
|
|
|
+You can configure this in a number of ways including:
|
|
|
|
|
|
-One customization strategy is to use an `ObjectPostProcessor`, which allows you to modify the
|
|
|
|
-objects created by the implementation. Another option is to override the authentication
|
|
|
|
-manager for the filter that intercepts the SAMLResponse.
|
|
|
|
|
|
+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
|
|
|
|
|
|
-[[servlet-saml2-opensamlauthenticationprovider]]
|
|
|
|
-==== OpenSamlAuthenticationProvider ObjectPostProcessor
|
|
|
|
|
|
+To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
|
|
|
|
+
|
|
|
|
+[[servlet-saml2-opensamlauthenticationprovider-authenticationmanager]]
|
|
|
|
+==== 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 `OpenSamlAuthenticationProvider` 's default assertion validator with some tolerance:
|
|
|
|
|
|
[source,java]
|
|
[source,java]
|
|
----
|
|
----
|
|
@@ -296,62 +575,113 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
|
|
|
@Override
|
|
@Override
|
|
protected void configure(HttpSecurity http) throws Exception {
|
|
protected void configure(HttpSecurity http) throws Exception {
|
|
- ObjectPostProcessor<OpenSamlAuthenticationProvider> processor = new ObjectPostProcessor<>() {
|
|
|
|
- @Override
|
|
|
|
- public <O extends OpenSamlAuthenticationProvider> O postProcess(O provider) {
|
|
|
|
- provider.setResponseTimeValidationSkew(RESPONSE_TIME_VALIDATION_SKEW);
|
|
|
|
- provider.setAuthoritiesMapper(AUTHORITIES_MAPPER);
|
|
|
|
- provider.setAuthoritiesExtractor(AUTHORITIES_EXTRACTOR);
|
|
|
|
- return provider;
|
|
|
|
- }
|
|
|
|
- };
|
|
|
|
|
|
+ OpenSamlAuthenticationProvider authenticationProvider = new OpenSamlAuthenticationProvider();
|
|
|
|
+ authenticationProvider.setAssertionValidator(OpenSamlAuthenticationProvider
|
|
|
|
+ .createDefaultAssertionValidator(assertionToken -> {
|
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
|
+ params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
|
|
|
|
+ // ... other validation parameters
|
|
|
|
+ return new ValidationContext(params);
|
|
|
|
+ })
|
|
|
|
+ );
|
|
|
|
|
|
http
|
|
http
|
|
- .authorizeRequests(authorize -> authorize
|
|
|
|
|
|
+ .authorizeRequests(authz -> authz
|
|
.anyRequest().authenticated()
|
|
.anyRequest().authenticated()
|
|
)
|
|
)
|
|
.saml2Login(saml2 -> saml2
|
|
.saml2Login(saml2 -> saml2
|
|
- .addObjectPostProcessor(processor)
|
|
|
|
- )
|
|
|
|
- ;
|
|
|
|
|
|
+ .authenticationManager(new ProviderManager(authenticationProvider))
|
|
|
|
+ );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
----
|
|
----
|
|
|
|
|
|
-[[servlet-saml2-opensamlauthenticationprovider-authenticationmanager]]
|
|
|
|
-==== Configure OpenSamlAuthenticationProvider as an Authentication Manager
|
|
|
|
-We can leverage the same method, `authenticationManager`, to override and customize the default
|
|
|
|
-`OpenSamlAuthenticationProvider`.
|
|
|
|
|
|
+==== 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:
|
|
|
|
|
|
[source,java]
|
|
[source,java]
|
|
----
|
|
----
|
|
@EnableWebSecurity
|
|
@EnableWebSecurity
|
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
|
|
+ @Autowired
|
|
|
|
+ UserDetailsService userDetailsService;
|
|
|
|
|
|
@Override
|
|
@Override
|
|
protected void configure(HttpSecurity http) throws Exception {
|
|
protected void configure(HttpSecurity http) throws Exception {
|
|
- OpenSamlAuthenticationProvider authProvider = new OpenSamlAuthenticationProvider();
|
|
|
|
- authProvider.setResponseTimeValidationSkew(RESPONSE_TIME_VALIDATION_SKEW);
|
|
|
|
- authProvider.setAuthoritiesMapper(AUTHORITIES_MAPPER);
|
|
|
|
- authProvider.setAuthoritiesExtractor(AUTHORITIES_EXTRACTOR);
|
|
|
|
|
|
+ OpenSamlAuthenticationProvider authenticationProvider = new OpenSamlAuthenticationProvider();
|
|
|
|
+ authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
|
|
|
|
+ Saml2Authentication authentication = OpenSamlAuthenticationProvider
|
|
|
|
+ .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
|
|
http
|
|
- .authorizeRequests(authorize -> authorize
|
|
|
|
|
|
+ .authorizeRequests(authz -> authz
|
|
.anyRequest().authenticated()
|
|
.anyRequest().authenticated()
|
|
)
|
|
)
|
|
.saml2Login(saml2 -> saml2
|
|
.saml2Login(saml2 -> saml2
|
|
- .authenticationManager(new ProviderManager(asList(authProvider)))
|
|
|
|
- )
|
|
|
|
- ;
|
|
|
|
|
|
+ .authenticationManager(new ProviderManager(authenticationProvider))
|
|
|
|
+ );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
----
|
|
----
|
|
|
|
+<1> First, call the default converter, which extracts attributes and authorities from the response
|
|
|
|
+<2> Second, call the `UserDetailsService` using the relevant information
|
|
|
|
+<3> Third, return a custom authentication that includes the user details
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+It's not required to call `OpenSamlAuthenticationProvider` 's default authentication converter.
|
|
|
|
+It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from `AttributeStatement` s as well as the single `ROLE_USER` authority.
|
|
|
|
+
|
|
|
|
+==== Performing additional validation
|
|
|
|
+
|
|
|
|
+`OpenSamlAuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
|
|
|
|
+After verifying the signature, it will:
|
|
|
|
+
|
|
|
|
+1. Validate `<AudienceRestriction>` and `<DelegationRestriction>` conditions
|
|
|
|
+2. Validate `<SubjectConfirmation>` s, expect for any IP address information
|
|
|
|
+
|
|
|
|
+To perform additional validation, you can configure your own assertion validator that delegates to `OpenSamlAuthenticationProvider` 's default and then performs its own.
|
|
|
|
+
|
|
|
|
+For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
|
|
|
|
+
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
|
|
|
+OneTimeUseConditionValidator validator = ...;
|
|
|
|
+provider.setAssertionValidator(assertionToken -> {
|
|
|
|
+ Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
|
|
|
|
+ .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.contact(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
|
|
|
|
+});
|
|
|
|
+----
|
|
|
|
+
|
|
|
|
+[NOTE]
|
|
|
|
+While recommended, it's not necessary to call `OpenSamlAuthenticationProvider` 's default assertion validator.
|
|
|
|
+A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself.
|
|
|
|
|
|
[[servlet-saml2-custom-authenticationmanager]]
|
|
[[servlet-saml2-custom-authenticationmanager]]
|
|
-==== Custom Authentication Manager
|
|
|
|
-The authentication manager for the security filter can also be overwritten, using your own
|
|
|
|
-custom `AuthenticationManager` implementation.
|
|
|
|
-This authentication manager should expect a `Saml2AuthenticationToken` object
|
|
|
|
-containing the SAML 2 Response XML data.
|
|
|
|
|
|
+==== Using a Custom Authentication Manager
|
|
|
|
+
|
|
|
|
+Of course, the `authenticationManager` DSL method can be used to perform a completely custom SAML 2.0 authentication.
|
|
|
|
+This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2 Response XML data.
|
|
|
|
|
|
[source,java]
|
|
[source,java]
|
|
----
|
|
----
|
|
@@ -373,90 +703,54 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|
}
|
|
}
|
|
----
|
|
----
|
|
|
|
|
|
-[[servlet-saml2-sample-boot]]
|
|
|
|
-=== Spring Boot 2.x Sample
|
|
|
|
-
|
|
|
|
-We are currently working with the Spring Boot team on the
|
|
|
|
-https://github.com/spring-projects/spring-boot/issues/18260[Auto Configuration for Spring Security SAML Login].
|
|
|
|
-In the meantime, we have provided a Spring Boot sample that supports a Yaml configuration.
|
|
|
|
|
|
+[[servlet-saml2login-authenticatedprincipal]]
|
|
|
|
+=== Using `Saml2AuthenticatedPrincipal`
|
|
|
|
|
|
-To run the sample, follow these three steps
|
|
|
|
|
|
+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`.
|
|
|
|
|
|
-1. Launch the Spring Boot application
|
|
|
|
-** `./gradlew :spring-security-samples-boot-saml2login:bootRun`
|
|
|
|
-2. Open a browser
|
|
|
|
-** http://localhost:8080/[http://localhost:8080/]
|
|
|
|
-3. This will take you to an identity provider, log in using:
|
|
|
|
-** User: `user`
|
|
|
|
-** Password: `password`
|
|
|
|
|
|
+This means that you can access the principal in your controller like so:
|
|
|
|
|
|
-[[servlet-saml2-sample-idps]]
|
|
|
|
-==== Multiple Identity Provider Sample
|
|
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+@Controller
|
|
|
|
+public class MainController {
|
|
|
|
+ @GetMapping("/")
|
|
|
|
+ public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
|
|
|
|
+ String email = principal.getFirstAttribute("email");
|
|
|
|
+ model.setAttribute("email", email);
|
|
|
|
+ return "index";
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+----
|
|
|
|
|
|
-It's very simple to use multiple providers, but there are some defaults that
|
|
|
|
-may trip you up if you don't pay attention. In our SAML configuration of
|
|
|
|
-`RelyingPartyRegistration` objects, we default an SP entity ID to
|
|
|
|
-`+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`
|
|
|
|
|
|
+[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.
|
|
|
|
|
|
-That means in our two provider configuration, our system would look like
|
|
|
|
|
|
+[[servlet-saml2login-metadata]]
|
|
|
|
+=== Publishing a Metadata Endpoint
|
|
|
|
|
|
-```
|
|
|
|
-registration-1 (Identity Provider 1) - Our local SP Entity ID is:
|
|
|
|
-http://localhost:8080/saml2/service-provider-metadata/registration-1
|
|
|
|
|
|
+You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
|
|
|
|
|
|
-registration-2 (Identity Provider 2) - Our local SP Entity ID is:
|
|
|
|
-http://localhost:8080/saml2/service-provider-metadata/registration-2
|
|
|
|
-```
|
|
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+Converter<HttpServletRequest, RelyingPartyRegistration> relyingPartyRegistrationResolver =
|
|
|
|
+ new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
|
|
|
|
+Saml2MetadataFilter filter = new Saml2MetadataFilter(
|
|
|
|
+ relyingPartyRegistrationResolver,
|
|
|
|
+ new OpenSamlMetadataResolver());
|
|
|
|
+
|
|
|
|
+http
|
|
|
|
+ // ...
|
|
|
|
+ .saml2Login(withDefaults())
|
|
|
|
+ .addFilterBefore(new Saml2MetadataFilter(r), Saml2WebSsoAuthenticationFilter.class);
|
|
|
|
+----
|
|
|
|
|
|
-In this configuration, illustrated in the sample below, to the outside world,
|
|
|
|
-we have actually created two virtual Service Provider identities
|
|
|
|
-hosted within the same application.
|
|
|
|
|
|
+By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`.
|
|
|
|
+You can change this by calling the `setRequestMatcher` method on the filter:
|
|
|
|
|
|
-[source,yaml]
|
|
|
|
|
|
+[source,java]
|
|
|
|
+----
|
|
|
|
+filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
|
|
----
|
|
----
|
|
-spring:
|
|
|
|
- security:
|
|
|
|
- saml2:
|
|
|
|
- login:
|
|
|
|
- relying-parties:
|
|
|
|
- - entity-id: &idp-entity-id https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php
|
|
|
|
- registration-id: simplesamlphp
|
|
|
|
- web-sso-url: &idp-sso-url https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php
|
|
|
|
- signing-credentials: &service-provider-credentials
|
|
|
|
- - private-key: |
|
|
|
|
- -----BEGIN PRIVATE KEY-----
|
|
|
|
- MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE
|
|
|
|
- ...................SHORTENED FOR READ ABILITY...................
|
|
|
|
- INrtuLp4YHbgk1mi
|
|
|
|
- -----END PRIVATE KEY-----
|
|
|
|
- certificate: |
|
|
|
|
- -----BEGIN CERTIFICATE-----
|
|
|
|
- MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC
|
|
|
|
- ...................SHORTENED FOR READ ABILITY...................
|
|
|
|
- RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B
|
|
|
|
- -----END CERTIFICATE-----
|
|
|
|
- verification-credentials: &idp-certificates
|
|
|
|
- - |
|
|
|
|
- -----BEGIN CERTIFICATE-----
|
|
|
|
- MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD
|
|
|
|
- ...................SHORTENED FOR READ ABILITY...................
|
|
|
|
- lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk
|
|
|
|
- -----END CERTIFICATE-----
|
|
|
|
- - entity-id: *idp-entity-id
|
|
|
|
- registration-id: simplesamlphp2
|
|
|
|
- web-sso-url: *idp-sso-url
|
|
|
|
- signing-credentials: *service-provider-credentials
|
|
|
|
- verification-credentials: *idp-certificates
|
|
|
|
-----
|
|
|
|
-
|
|
|
|
-If this is not desirable, you can manually override the local SP entity ID by using the
|
|
|
|
-
|
|
|
|
-[source,attrs="-attributes"]
|
|
|
|
-----
|
|
|
|
-localEntityIdTemplate = {baseUrl}/saml2/service-provider-metadata
|
|
|
|
-----
|
|
|
|
-
|
|
|
|
-If we change our local SP entity ID to this value, it is still important that we give
|
|
|
|
-out the correct single sign on URL (the assertion consumer service URL)
|
|
|
|
-for each registered identity provider based on the registration Id.
|
|
|
|
-`+{baseUrl}/login/saml2/sso/{registrationId}+`
|
|
|