1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540 |
- [[servlet-saml2login]]
- = SAML 2.0 Login
- :figures: images/servlet/saml2
- :icondir: images/icons
- The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users https://wiki.shibboleth.net/confluence/display/CONCEPT/FlowsAndConfig[log in] to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc).
- NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in
- https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles].
- [[servlet-saml2login-spring-security-history]]
- Since 2009, support for relying parties has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project].
- In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper.
- This process is similar to the one started in 2017 for 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[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 <<servlet-saml2login-sp-initiated-factory,the `<saml2:AuthnRequest>` generating endpoint>>, `Saml2WebSsoAuthenticationRequestFilter`.
- Or, if you've <<servlet-saml2login-relyingpartyregistrationrepository,configured more than one asserting party>>, it will first redirect to a picker page.
- image:{icondir}/number_4.png[] Next, the `Saml2WebSsoAuthenticationRequestFilter` creates, signs, serializes, and encodes a `<saml2:AuthnRequest>` using its configured <<servlet-saml2login-sp-initiated-factory,`Saml2AuthenticationRequestFactory`>>.
- image:{icondir}/number_5.png[] Then, the browser takes this `<saml2:AuthnRequest>` and presents it to the asserting party.
- The asserting party attempts to authentication the user.
- If successful, it will return a `<saml2:Response>` back to the browser.
- image:{icondir}/number_6.png[] The browser then POSTs the `<saml2:Response>` to the assertion consumer service endpoint.
- [[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
- .Authenticating a `<saml2:Response>`
- image::{figures}/saml2webssoauthenticationfilter.png[]
- The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
- image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it <<servlet-saml2login-authenticate-responses, delegates to `Saml2WebSsoAuthenticationFilter`>>.
- This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
- This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
- image:{icondir}/number_2.png[] Next, the filter passes the token to its configured xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`].
- By default, it will use the <<servlet-saml2login-architecture,`OpenSAML authentication provider`>>.
- image:{icondir}/number_3.png[] If authentication fails, then __Failure__
- * The 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 xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[registered the relying party with your asserting party].
- === Specifying Identity Provider Metadata
- In a Spring Boot application, to specify an identity provider's metadata, simply do:
- [source,yml]
- ----
- spring:
- security:
- saml2:
- relyingparty:
- registration:
- adfs:
- identityprovider:
- entity-id: https://idp.example.com/issuer
- verification.credentials:
- - certificate-location: "classpath:idp.crt"
- singlesignon.url: https://idp.example.com/issuer/sso
- singlesignon.sign-request: false
- ----
- where
- * `https://idp.example.com/issuer` is the value contained in the `Issuer` attribute of the SAML responses that the identity provider will issue
- * `classpath:idp.crt` is the location on the classpath for the identity provider's certificate for verifying SAML responses, and
- * `https://idp.example.com/issuer/sso` is the endpoint where the identity provider is expecting ``AuthnRequest``s.
- * `adfs` is <<servlet-saml2login-relyingpartyregistrationid, an arbitrary identifier you choose>>
- And that's it!
- [NOTE]
- Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party.
- These are frequently abbreviated as AP and RP, respectively.
- === Runtime Expectations
- As configured above, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
- [source,html]
- ----
- POST /login/saml2/sso/adfs HTTP/1.1
- SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
- ----
- There are two ways to see induce your asserting party to generate a `SAMLResponse`:
- * First, you can navigate to your asserting party.
- It likely has some kind of link or button for each registered relying party that you can click to send the `SAMLResponse`.
- * Second, you can navigate to a protected page in your app, for example, `http://localhost:8080`.
- Your app then redirects to the configured asserting party which then sends the `SAMLResponse`.
- From here, consider jumping to:
- * <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
- * <<servlet-saml2login-authenticatedprincipal,How to Use the `Saml2AuthenticatedPrincipal`>>
- * <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
- [[servlet-saml2login-architecture]]
- == How SAML 2.0 Login Integrates with OpenSAML
- Spring Security's SAML 2.0 support has a couple of design goals:
- * First, rely on a library for SAML 2.0 operations and domain objects.
- To achieve this, Spring Security uses OpenSAML.
- * Second, ensure this library is not required when using Spring Security's SAML support.
- To achieve this, any interfaces or classes where Spring Security uses OpenSAML in the contract remain encapsulated.
- This makes it possible for you to switch out OpenSAML for some other library or even an unsupported version of OpenSAML.
- As a natural outcome of the above two goals, Spring Security's SAML API is quite small relative to other modules.
- Instead, classes like `OpenSaml4AuthenticationRequestFactory` and `OpenSaml4AuthenticationProvider` expose ``Converter``s that customize various steps in the authentication process.
- For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter will delegate to `OpenSaml4AuthenticationProvider`.
- [NOTE]
- For backward compatibility, Spring Security will use the latest OpenSAML 3 by default.
- Note, though that OpenSAML 3 has reached it's end-of-life and updating to OpenSAML 4.x is recommended.
- For that reason, Spring Security supports both OpenSAML 3.x and 4.x.
- If you manage your OpenSAML dependency to 4.x, then Spring Security will select its OpenSAML 4.x implementations.
- .Authenticating an OpenSAML `Response`
- image:{figures}/opensamlauthenticationprovider.png[]
- This figure builds off of the <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter` diagram>>.
- image:{icondir}/number_1.png[] The `Saml2WebSsoAuthenticationFilter` formulates the `Saml2AuthenticationToken` and invokes the 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 <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedAssertion` elements>>.
- If any decryptions fail, authentication fails.
- image:{icondir}/number_5.png[] Next, the provider validates the response's `Issuer` and `Destination` values.
- If they don't match what's in the `RelyingPartyRegistration`, authentication fails.
- image:{icondir}/number_6.png[] After that, the provider verifies the signature of each `Assertion`.
- If any signature is invalid, authentication fails.
- Also, if neither the response nor the assertions have signatures, authentication fails.
- Either the response or all the assertions must have signatures.
- image:{icondir}/number_7.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedID` or `EncryptedAttribute` elements>>.
- If any decryptions fail, authentication fails.
- image:{icondir}/number_8.png[] Next, the provider validates each assertion's `ExpiresAt` and `NotBefore` timestamps, the `<Subject>` and any `<AudienceRestriction>` conditions.
- If any validations fail, authentication fails.
- image:{icondir}/number_9.png[] Following that, the provider takes the first assertion's `AttributeStatement` and maps it to a `Map<String, List<Object>>`.
- It also grants the `ROLE_USER` granted authority.
- image:{icondir}/number_10.png[] And finally, it takes the `NameID` from the first assertion, the `Map` of attributes, and the `GrantedAuthority` and constructs a `Saml2AuthenticatedPrincipal`.
- Then, it places that principal and the authorities into a `Saml2Authentication`.
- The resulting `Authentication#getPrincipal` is a Spring Security `Saml2AuthenticatedPrincipal` object, and `Authentication#getName` maps to the first assertion's `NameID` element.
- `Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId` holds the <<servlet-saml2login-relyingpartyregistrationid,identifier to the associated `RelyingPartyRegistration`>>.
- [[servlet-saml2login-opensaml-customization]]
- === Customizing OpenSAML Configuration
- Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class, 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)
- }
- ----
- ====
- [[servlet-saml2login-relyingpartyregistrationid]]
- [NOTE]
- The `registrationId` is an arbitrary value that you choose for differentiating between registrations.
- Or you can provide each detail manually, as you can see below:
- .Relying Party Registration Repository Manual Configuration
- ====
- .Java
- [source,java,role="primary"]
- ----
- @Value("${verification.key}")
- File verificationKey;
- @Bean
- public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
- X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
- Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
- RelyingPartyRegistration registration = RelyingPartyRegistration
- .withRegistrationId("example")
- .assertingPartyDetails(party -> party
- .entityId("https://idp.example.com/issuer")
- .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
- .wantAuthnRequestsSigned(false)
- .verificationX509Credentials(c -> c.add(credential))
- )
- .build();
- return new InMemoryRelyingPartyRegistrationRepository(registration);
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @Value("\${verification.key}")
- var verificationKey: File? = null
- @Bean
- open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
- val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
- val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
- val registration = RelyingPartyRegistration
- .withRegistrationId("example")
- .assertingPartyDetails { party: AssertingPartyDetails.Builder ->
- party
- .entityId("https://idp.example.com/issuer")
- .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
- .wantAuthnRequestsSigned(false)
- .verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
- c.add(
- credential
- )
- }
- }
- .build()
- return InMemoryRelyingPartyRegistrationRepository(registration)
- }
- ----
- ====
- [NOTE]
- Note that `X509Support` is an OpenSAML class, used here in the snippet for brevity
- [[servlet-saml2login-relyingpartyregistrationrepository-dsl]]
- Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configured `WebSecurityConfigurerAdapter`:
- .Custom Relying Party Registration DSL
- ====
- .Java
- [source,java,role="primary"]
- ----
- @EnableWebSecurity
- public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
- protected void configure(HttpSecurity http) {
- http
- .authorizeRequests(authorize -> authorize
- .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
- .anyRequest().authenticated()
- )
- .saml2Login(saml2 -> saml2
- .relyingPartyRegistrationRepository(relyingPartyRegistrations())
- );
- }
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @EnableWebSecurity
- class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
- override fun configure(http: HttpSecurity) {
- http {
- authorizeRequests {
- authorize("/messages/**", hasAuthority("ROLE_USER"))
- authorize(anyRequest, authenticated)
- }
- saml2Login {
- relyingPartyRegistrationRepository = relyingPartyRegistrations()
- }
- }
- }
- }
- ----
- ====
- [NOTE]
- A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`.
- [[servlet-saml2login-relyingpartyregistration]]
- == RelyingPartyRegistration
- A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
- instance represents a link between an relying party and assering party's metadata.
- In a `RelyingPartyRegistration`, you can provide relying party metadata like its `Issuer` value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads.
- Also, you can provide asserting party metadata like its `Issuer` value, where it expects AuthnRequests to be sent to, and any public credentials that it owns for the purposes of the relying party verifying or encrypting payloads.
- The following `RelyingPartyRegistration` is the minimum required for most setups:
- ====
- .Java
- [source,java,role="primary"]
- ----
- RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
- .fromMetadataLocation("https://ap.example.org/metadata")
- .registrationId("my-id")
- .build();
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- val relyingPartyRegistration = RelyingPartyRegistrations
- .fromMetadataLocation("https://ap.example.org/metadata")
- .registrationId("my-id")
- .build()
- ----
- ====
- Note that you can also create a `RelyingPartyRegistration` from an arbitrary `InputStream` source.
- One such example is when the metadata is stored in a database:
- [source,java]
- ----
- String xml = fromDatabase();
- try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
- RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
- .fromMetadata(source)
- .registrationId("my-id")
- .build();
- }
- ----
- Though a more sophisticated setup is also possible, like so:
- ====
- .Java
- [source,java,role="primary"]
- ----
- RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
- .entityId("{baseUrl}/{registrationId}")
- .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
- .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
- .assertingPartyDetails(party -> party
- .entityId("https://ap.example.org")
- .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
- .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
- )
- .build();
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- val relyingPartyRegistration =
- RelyingPartyRegistration.withRegistrationId("my-id")
- .entityId("{baseUrl}/{registrationId}")
- .decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
- c.add(relyingPartyDecryptingCredential())
- }
- .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
- .assertingPartyDetails { party -> party
- .entityId("https://ap.example.org")
- .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
- .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
- }
- .build()
- ----
- ====
- [TIP]
- The top-level metadata methods are details about the relying party.
- The methods inside `assertingPartyDetails` are details about the asserting party.
- [NOTE]
- The location where a relying party is expecting SAML Responses is the Assertion Consumer Service Location.
- The default for the relying party's `entityId` is `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`.
- This is this value needed when configuring the asserting party to know about your relying party.
- The default for the `assertionConsumerServiceLocation` is `+/login/saml2/sso/{registrationId}+`.
- It's mapped by default to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
- [[servlet-saml2login-rpr-uripatterns]]
- === URI Patterns
- You probably noticed in the above examples the `+{baseUrl}+` and `+{registrationId}+` placeholders.
- These are useful for generating URIs. As such, the relying party's `entityId` and `assertionConsumerServiceLocation` support the following placeholders:
- * `baseUrl` - the scheme, host, and port of a deployed application
- * `registrationId` - the registration id for this relying party
- * `baseScheme` - the scheme of a deployed application
- * `baseHost` - the host of a deployed application
- * `basePort` - the port of a deployed application
- For example, the `assertionConsumerServiceLocation` defined above was:
- `+/my-login-endpoint/{registrationId}+`
- which in a deployed application would translate to
- `+/my-login-endpoint/adfs+`
- The `entityId` above was defined as:
- `+{baseUrl}/{registrationId}+`
- which in a deployed application would translate to
- `+https://rp.example.com/adfs+`
- [[servlet-saml2login-rpr-credentials]]
- === Credentials
- You also likely noticed the credential that was used.
- Oftentimes, a relying party will use the same key to sign payloads as well as decrypt them.
- Or it will use the same key to verify payloads as well as encrypt them.
- Because of this, Spring Security ships with `Saml2X509Credential`, a SAML-specific credential that simplifies configuring the same key for different use cases.
- At a minimum, it's necessary to have a certificate from the asserting party so that the asserting party's signed responses can be verified.
- To construct a `Saml2X509Credential` that you'll use to verify assertions from the asserting party, you can load the file and use
- the `CertificateFactory` like so:
- ====
- .Java
- [source,java,role="primary"]
- ----
- Resource resource = new ClassPathResource("ap.crt");
- try (InputStream is = resource.getInputStream()) {
- X509Certificate certificate = (X509Certificate)
- CertificateFactory.getInstance("X.509").generateCertificate(is);
- return Saml2X509Credential.verification(certificate);
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- val resource = ClassPathResource("ap.crt")
- resource.inputStream.use {
- return Saml2X509Credential.verification(
- CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
- )
- }
- ----
- ====
- Let's say that the asserting party is going to also encrypt the assertion.
- In that case, the relying party will need a private key to be able to decrypt the encrypted value.
- In that case, you'll need an `RSAPrivateKey` as well as its corresponding `X509Certificate`.
- You can load the first using Spring Security's `RsaKeyConverters` utility class and the second as you did before:
- ====
- .Java
- [source,java,role="primary"]
- ----
- X509Certificate certificate = relyingPartyDecryptionCertificate();
- Resource resource = new ClassPathResource("rp.crt");
- try (InputStream is = resource.getInputStream()) {
- RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
- return Saml2X509Credential.decryption(rsa, certificate);
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- val certificate: X509Certificate = relyingPartyDecryptionCertificate()
- val resource = ClassPathResource("rp.crt")
- resource.inputStream.use {
- val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
- return Saml2X509Credential.decryption(rsa, certificate)
- }
- ----
- ====
- [TIP]
- When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
- [[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
- === Resolving the Relying Party from the Request
- As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
- There are a number of reasons you may want to customize. Among them:
- * You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme
- * You may identify tenants in a way other than by the URI path
- To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
- 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 RelyingPartyRegistrationResolver {
- private final RelyingPartyRegistrationResolver delegate;
- public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
- this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
- }
- @Override
- public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
- return this.delegate.resolve(request, "single");
- }
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
- override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
- return this.delegate.resolve(request, "single")
- }
- }
- ----
- ====
- Then, you can provide this resolver to the appropriate filters that <<servlet-saml2login-sp-initiated-factory, produce ``<saml2:AuthnRequest>``s>>, <<servlet-saml2login-authenticate-responses, authenticate ``<saml2:Response>``s>>, and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
- [NOTE]
- Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
- [[servlet-saml2login-rpr-duplicated]]
- === Duplicated Relying Party Configurations
- When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances:
- * The relying party's `entityId`
- * Its `assertionConsumerServiceLocation`, and
- * Its credentials, for example its signing or decryption credentials
- What's nice about this setup is credentials may be more easily rotated for some identity providers vs others.
- The duplication can be alleviated in a few different ways.
- First, in YAML this can be alleviated with references, like so:
- [source,yaml]
- ----
- spring:
- security:
- saml2:
- relyingparty:
- okta:
- signing.credentials: &relying-party-credentials
- - private-key-location: classpath:rp.key
- certificate-location: classpath:rp.crt
- identityprovider:
- entity-id: ...
- azure:
- signing.credentials: *relying-party-credentials
- identityprovider:
- entity-id: ...
- ----
- Second, in a database, it's not necessary to replicate `RelyingPartyRegistration` 's model.
- Third, in Java, you can create a custom configuration method, like so:
- ====
- .Java
- [source,java,role="primary"]
- ----
- private RelyingPartyRegistration.Builder
- addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {
- Saml2X509Credential signingCredential = ...
- builder.signingX509Credentials(c -> c.addAll(signingCredential));
- // ... other relying party configurations
- }
- @Bean
- public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
- RelyingPartyRegistration okta = addRelyingPartyDetails(
- RelyingPartyRegistrations
- .fromMetadataLocation(oktaMetadataUrl)
- .registrationId("okta")).build();
- RelyingPartyRegistration azure = addRelyingPartyDetails(
- RelyingPartyRegistrations
- .fromMetadataLocation(oktaMetadataUrl)
- .registrationId("azure")).build();
- return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
- val signingCredential: Saml2X509Credential = ...
- builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
- c.add(
- signingCredential
- )
- }
- // ... other relying party configurations
- }
- @Bean
- open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
- val okta = addRelyingPartyDetails(
- RelyingPartyRegistrations
- .fromMetadataLocation(oktaMetadataUrl)
- .registrationId("okta")
- ).build()
- val azure = addRelyingPartyDetails(
- RelyingPartyRegistrations
- .fromMetadataLocation(oktaMetadataUrl)
- .registrationId("azure")
- ).build()
- return InMemoryRelyingPartyRegistrationRepository(okta, azure)
- }
- ----
- ====
- [[servlet-saml2login-sp-initiated-factory]]
- == Producing ``<saml2:AuthnRequest>``s
- As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party.
- Spring Security achieves this in part by registering the `Saml2WebSsoAuthenticationRequestFilter` in the filter chain.
- This filter by default responds to endpoint `+/saml2/authenticate/{registrationId}+`.
- For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to:
- `https://rp.example.org/saml2/authenticate/ping`
- and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
- [[servlet-saml2login-store-authn-request]]
- === Changing How the `<saml2:AuthnRequest>` Gets Stored
- `Saml2WebSsoAuthenticationRequestFilter` uses an `Saml2AuthenticationRequestRepository` to persist an `AbstractSaml2AuthenticationRequest` instance before <<servlet-saml2login-sp-initiated-factory,sending the `<saml2:AuthnRequest>`>> to the asserting party.
- Additionally, `Saml2WebSsoAuthenticationFilter` and `Saml2AuthenticationTokenConverter` use an `Saml2AuthenticationRequestRepository` to load any `AbstractSaml2AuthenticationRequest` as part of <<servlet-saml2login-authenticate-responses,authenticating the `<saml2:Response>`>>.
- By default, Spring Security uses an `HttpSessionSaml2AuthenticationRequestRepository`, which stores the `AbstractSaml2AuthenticationRequest` in the `HttpSession`.
- If you have a custom implementation of `Saml2AuthenticationRequestRepository`, you may configure it by exposing it as a `@Bean` as shown in the following example:
- ====
- .Java
- [source,java,role="primary"]
- ----
- @Bean
- Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() {
- return new CustomSaml2AuthenticationRequestRepository();
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @Bean
- open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
- return CustomSaml2AuthenticationRequestRepository()
- }
- ----
- ====
- [[servlet-saml2login-sp-initiated-factory-signing]]
- === Changing How the `<saml2:AuthnRequest>` Gets Sent
- By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party.
- Many asserting parties don't require a signed `<saml2:AuthnRequest>`.
- This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
- .Not Requiring Signed AuthnRequests
- ====
- .Boot
- [source,yaml,role="primary"]
- ----
- spring:
- security:
- saml2:
- relyingparty:
- okta:
- identityprovider:
- entity-id: ...
- singlesignon.sign-request: false
- ----
- .Java
- [source,java,role="secondary"]
- ----
- RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
- // ...
- .assertingPartyDetails(party -> party
- // ...
- .wantAuthnRequestsSigned(false)
- )
- .build();
- ----
- .Kotlin
- [source,java,role="secondary"]
- ----
- var relyingPartyRegistration: RelyingPartyRegistration =
- RelyingPartyRegistration.withRegistrationId("okta")
- // ...
- .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
- // ...
- .wantAuthnRequestsSigned(false)
- }
- .build();
- ----
- ====
- Otherwise, you will need to specify a private key to `RelyingPartyRegistration#signingX509Credentials` so that Spring Security can sign the `<saml2:AuthnRequest>` before sending.
- [[servlet-saml2login-sp-initiated-factory-algorithm]]
- By default, Spring Security will sign the `<saml2:AuthnRequest>` using `rsa-sha256`, though some asserting parties will require a different algorithm, as indicated in their metadata.
- You can configure the algorithm based on the asserting party's <<servlet-saml2login-relyingpartyregistrationrepository,metadata using `RelyingPartyRegistrations`>>.
- Or, you can provide it manually:
- ====
- .Java
- [source,java,role="primary"]
- ----
- String metadataLocation = "classpath:asserting-party-metadata.xml";
- RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
- // ...
- .assertingPartyDetails((party) -> party
- // ...
- .signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
- )
- .build();
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- var metadataLocation = "classpath:asserting-party-metadata.xml"
- var relyingPartyRegistration: RelyingPartyRegistration =
- RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
- // ...
- .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
- // ...
- .signingAlgorithms { sign: MutableList<String?> ->
- sign.add(
- SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512
- )
- }
- }
- .build();
- ----
- ====
- NOTE: The snippet above uses the OpenSAML `SignatureConstants` class to supply the algorithm name.
- But, that's just for convenience.
- Since the datatype is `String`, you can supply the name of the algorithm directly.
- [[servlet-saml2login-sp-initiated-factory-binding]]
- Some asserting parties require that the `<saml2:AuthnRequest>` be POSTed.
- This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
- ====
- .Java
- [source,java,role="primary"]
- ----
- RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
- // ...
- .assertingPartyDetails(party -> party
- // ...
- .singleSignOnServiceBinding(Saml2MessageBinding.POST)
- )
- .build();
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- var relyingPartyRegistration: RelyingPartyRegistration? =
- RelyingPartyRegistration.withRegistrationId("okta")
- // ...
- .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
- // ...
- .singleSignOnServiceBinding(Saml2MessageBinding.POST)
- }
- .build()
- ----
- ====
- [[servlet-saml2login-sp-initiated-factory-custom-authnrequest]]
- === Customizing OpenSAML's `AuthnRequest` Instance
- There are a number of reasons that you may want to adjust an `AuthnRequest`.
- For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
- If you don't need information from the `HttpServletRequest` to make your decision, then the easiest way is to <<servlet-saml2login-opensaml-customization,register a custom `AuthnRequestMarshaller` with OpenSAML>>.
- This will give you access to post-process the `AuthnRequest` instance before it's serialized.
- But, if you do need something from the request, then you can use create a custom `Saml2AuthenticationRequestContext` implementation and then a `Converter<Saml2AuthenticationRequestContext, AuthnRequest>` to build an `AuthnRequest` yourself, like so:
- ====
- .Java
- [source,java,role="primary"]
- ----
- @Component
- public class AuthnRequestConverter implements
- Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
- private final AuthnRequestBuilder authnRequestBuilder;
- private final IssuerBuilder issuerBuilder;
- // ... constructor
- public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
- MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context;
- Issuer issuer = issuerBuilder.buildObject();
- issuer.setValue(myContext.getIssuer());
- AuthnRequest authnRequest = authnRequestBuilder.buildObject();
- authnRequest.setIssuer(issuer);
- authnRequest.setDestination(myContext.getDestination());
- authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl());
- // ... additional settings
- authRequest.setForceAuthn(myContext.getForceAuthn());
- return authnRequest;
- }
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @Component
- class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
- private val authnRequestBuilder: AuthnRequestBuilder? = null
- private val issuerBuilder: IssuerBuilder? = null
- // ... constructor
- override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
- val myContext: MySaml2AuthenticationRequestContext = context
- val issuer: Issuer = issuerBuilder.buildObject()
- issuer.value = myContext.getIssuer()
- val authnRequest: AuthnRequest = authnRequestBuilder.buildObject()
- authnRequest.issuer = issuer
- authnRequest.destination = myContext.getDestination()
- authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl()
- // ... additional settings
- authRequest.setForceAuthn(myContext.getForceAuthn())
- return authnRequest
- }
- }
- ----
- ====
- Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as ``@Bean``s:
- ====
- .Java
- [source,java,role="primary"]
- ----
- @Bean
- Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() {
- Saml2AuthenticationRequestContextResolver resolver =
- new DefaultSaml2AuthenticationRequestContextResolver();
- return request -> {
- Saml2AuthenticationRequestContext context = resolver.resolve(request);
- return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null);
- };
- }
- @Bean
- Saml2AuthenticationRequestFactory authenticationRequestFactory(
- AuthnRequestConverter authnRequestConverter) {
- OpenSaml4AuthenticationRequestFactory authenticationRequestFactory =
- new OpenSaml4AuthenticationRequestFactory();
- authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
- return authenticationRequestFactory;
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @Bean
- open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver {
- val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver()
- return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest ->
- val context = resolver.resolve(request)
- MySaml2AuthenticationRequestContext(
- context,
- request.getParameter("force") != null
- )
- }
- }
- @Bean
- open fun authenticationRequestFactory(
- authnRequestConverter: AuthnRequestConverter?
- ): Saml2AuthenticationRequestFactory? {
- val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory()
- authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter)
- return authenticationRequestFactory
- }
- ----
- ====
- [[servlet-saml2login-authenticate-responses]]
- == Authenticating ``<saml2:Response>``s
- To verify SAML 2.0 Responses, Spring Security uses <<servlet-saml2login-architecture,`OpenSaml4AuthenticationProvider`>> by default.
- You can configure this in a number of ways including:
- 1. Setting a clock skew to timestamp validation
- 2. Mapping the response to a list of `GrantedAuthority` instances
- 3. Customizing the strategy for validating assertions
- 4. Customizing the strategy for decrypting response and assertion elements
- To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
- [[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
- === Setting a Clock Skew
- It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized.
- For that reason, you can configure `OpenSaml4AuthenticationProvider` 's default assertion validator with some tolerance:
- ====
- .Java
- [source,java,role="primary"]
- ----
- @EnableWebSecurity
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
- authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
- .createDefaultAssertionValidator(assertionToken -> {
- Map<String, Object> params = new HashMap<>();
- params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
- // ... other validation parameters
- return new ValidationContext(params);
- })
- );
- http
- .authorizeRequests(authz -> authz
- .anyRequest().authenticated()
- )
- .saml2Login(saml2 -> saml2
- .authenticationManager(new ProviderManager(authenticationProvider))
- );
- }
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @EnableWebSecurity
- open class SecurityConfig : WebSecurityConfigurerAdapter() {
- override fun configure(http: HttpSecurity) {
- val authenticationProvider = OpenSaml4AuthenticationProvider()
- authenticationProvider.setAssertionValidator(
- OpenSaml4AuthenticationProvider
- .createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
- val params: MutableMap<String, Any> = HashMap()
- params[CLOCK_SKEW] =
- Duration.ofMinutes(10).toMillis()
- ValidationContext(params)
- })
- )
- http {
- authorizeRequests {
- authorize(anyRequest, authenticated)
- }
- saml2Login {
- authenticationManager = ProviderManager(authenticationProvider)
- }
- }
- }
- }
- ----
- ====
- [[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
- === Coordinating with a `UserDetailsService`
- Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
- In that case, the response authentication converter can come in handy, as can be seen below:
- ====
- .Java
- [source,java,role="primary"]
- ----
- @EnableWebSecurity
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- @Autowired
- UserDetailsService userDetailsService;
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
- authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
- Saml2Authentication authentication = OpenSaml4AuthenticationProvider
- .createDefaultResponseAuthenticationConverter() <1>
- .convert(responseToken);
- Assertion assertion = responseToken.getResponse().getAssertions().get(0);
- String username = assertion.getSubject().getNameID().getValue();
- UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); <2>
- return MySaml2Authentication(userDetails, authentication); <3>
- });
- http
- .authorizeRequests(authz -> authz
- .anyRequest().authenticated()
- )
- .saml2Login(saml2 -> saml2
- .authenticationManager(new ProviderManager(authenticationProvider))
- );
- }
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @EnableWebSecurity
- open class SecurityConfig : WebSecurityConfigurerAdapter() {
- @Autowired
- var userDetailsService: UserDetailsService? = null
- override fun configure(http: HttpSecurity) {
- val authenticationProvider = OpenSaml4AuthenticationProvider()
- authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
- val authentication = OpenSaml4AuthenticationProvider
- .createDefaultResponseAuthenticationConverter() <1>
- .convert(responseToken)
- val assertion: Assertion = responseToken.response.assertions[0]
- val username: String = assertion.subject.nameID.value
- val userDetails = userDetailsService!!.loadUserByUsername(username) <2>
- MySaml2Authentication(userDetails, authentication) <3>
- }
- http {
- authorizeRequests {
- authorize(anyRequest, authenticated)
- }
- saml2Login {
- authenticationManager = ProviderManager(authenticationProvider)
- }
- }
- }
- }
- ----
- ====
- <1> First, call the default converter, which extracts attributes and authorities from the response
- <2> Second, call the 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 `<AudienceRestriction>` and `<DelegationRestriction>` conditions
- 2. Validate ``<SubjectConfirmation>``s, expect for any IP address information
- To perform additional validation, you can configure your own assertion validator that delegates to `OpenSaml4AuthenticationProvider` 's default and then performs its own.
- [[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]]
- For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
- ====
- .Java
- [source,java,role="primary"]
- ----
- OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
- OneTimeUseConditionValidator validator = ...;
- provider.setAssertionValidator(assertionToken -> {
- Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
- .createDefaultAssertionValidator()
- .convert(assertionToken);
- Assertion assertion = assertionToken.getAssertion();
- OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
- ValidationContext context = new ValidationContext();
- try {
- if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
- return result;
- }
- } catch (Exception e) {
- return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
- }
- return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
- });
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- var provider = OpenSaml4AuthenticationProvider()
- var validator: OneTimeUseConditionValidator = ...
- provider.setAssertionValidator { assertionToken ->
- val result = OpenSaml4AuthenticationProvider
- .createDefaultAssertionValidator()
- .convert(assertionToken)
- val assertion: Assertion = assertionToken.assertion
- val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
- val context = ValidationContext()
- try {
- if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
- return@setAssertionValidator result
- }
- } catch (e: Exception) {
- return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
- }
- result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
- }
- ----
- ====
- [NOTE]
- While recommended, it's not necessary to call `OpenSaml4AuthenticationProvider` 's default assertion validator.
- A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself.
- [[servlet-saml2login-opensamlauthenticationprovider-decryption]]
- === Customizing Decryption
- Spring Security decrypts `<saml2:EncryptedAssertion>`, `<saml2:EncryptedAttribute>`, and `<saml2:EncryptedID>` elements automatically by using the decryption <<servlet-saml2login-rpr-credentials,`Saml2X509Credential` instances>> registered in the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>.
- `OpenSaml4AuthenticationProvider` exposes <<servlet-saml2login-architecture,two decryption strategies>>.
- The response decrypter is for decrypting encrypted elements of the `<saml2:Response>`, like `<saml2:EncryptedAssertion>`.
- The assertion decrypter is for decrypting encrypted elements of the `<saml2:Assertion>`, like `<saml2:EncryptedAttribute>` and `<saml2:EncryptedID>`.
- You can replace `OpenSaml4AuthenticationProvider`'s default decryption strategy with your own.
- For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>`, you can use it instead like so:
- ====
- .Java
- [source,java,role="primary"]
- ----
- MyDecryptionService decryptionService = ...;
- OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
- provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- val decryptionService: MyDecryptionService = ...
- val provider = OpenSaml4AuthenticationProvider()
- provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
- ----
- ====
- If you are also decrypting individual elements in a `<saml2:Assertion>`, you can customize the assertion decrypter, too:
- ====
- .Java
- [source,java,role="primary"]
- ----
- provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
- ----
- ====
- NOTE: There are two separate decrypters since assertions can be signed separately from responses.
- Trying to decrypt a signed assertion's elements before signature verification may invalidate the signature.
- If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter.
- [[servlet-saml2login-authenticationmanager-custom]]
- === Using a Custom Authentication Manager
- [[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]]
- Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication.
- This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2.0 Response XML data.
- ====
- .Java
- [source,java,role="primary"]
- ----
- @EnableWebSecurity
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
- http
- .authorizeRequests(authorize -> authorize
- .anyRequest().authenticated()
- )
- .saml2Login(saml2 -> saml2
- .authenticationManager(authenticationManager)
- )
- ;
- }
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @EnableWebSecurity
- open class SecurityConfig : WebSecurityConfigurerAdapter() {
- override fun configure(http: HttpSecurity) {
- val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
- http {
- authorizeRequests {
- authorize(anyRequest, authenticated)
- }
- saml2Login {
- authenticationManager = customAuthenticationManager
- }
- }
- }
- }
- ----
- ====
- [[servlet-saml2login-authenticatedprincipal]]
- == Using `Saml2AuthenticatedPrincipal`
- With the relying party correctly configured for a given asserting party, it's ready to accept assertions.
- Once the relying party validates an assertion, the result is a `Saml2Authentication` with a `Saml2AuthenticatedPrincipal`.
- This means that you can access the principal in your controller like so:
- ====
- .Java
- [source,java,role="primary"]
- ----
- @Controller
- public class MainController {
- @GetMapping("/")
- public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
- String email = principal.getFirstAttribute("email");
- model.setAttribute("email", email);
- return "index";
- }
- }
- ----
- .Kotlin
- [source,kotlin,role="secondary"]
- ----
- @Controller
- class MainController {
- @GetMapping("/")
- fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
- val email = principal.getFirstAttribute<String>("email")
- model.setAttribute("email", email)
- return "index"
- }
- }
- ----
- ====
- [TIP]
- Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call `getAttribute` to get the list of attributes or `getFirstAttribute` to get the first in the list.
- `getFirstAttribute` is quite handy when you know that there is only one value.
|