index.adoc 62 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618
  1. [[servlet-saml2]]
  2. = SAML2
  3. Spring Security provides comprehensive SAML 2 support.
  4. This section discusses how to integrate SAML 2 into your servlet based application.
  5. [[servlet-saml2login]]
  6. == SAML 2.0 Login
  7. :figures: images/servlet/saml2
  8. :icondir: images/icons
  9. 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).
  10. NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in
  11. https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles].
  12. [[servlet-saml2login-spring-security-history]]
  13. Since 2009, support for relying parties has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project].
  14. In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper.
  15. This process is similar to the one started in 2017 for <<oauth2,Spring Security's OAuth 2.0 support>>.
  16. [NOTE]
  17. ====
  18. 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].
  19. ====
  20. Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security.
  21. First, we see that, like <<oauth2login, OAuth 2.0 Login>>, Spring Security takes the user to a third-party for performing authentication.
  22. It does this through a series of redirects.
  23. .Redirecting to Asserting Party Authentication
  24. image::{figures}/saml2webssoauthenticationrequestfilter.png[]
  25. The figure above builds off our <<servlet-securityfilterchain,`SecurityFilterChain`>> and <<servlet-authentication-abstractprocessingfilter, `AbstractAuthenticationProcessingFilter`>> diagrams:
  26. image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized.
  27. image:{icondir}/number_2.png[] Spring Security's <<servlet-authorization-filtersecurityinterceptor,`FilterSecurityInterceptor`>> indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`.
  28. image:{icondir}/number_3.png[] Since the user lacks authorization, the <<servlet-exceptiontranslationfilter,`ExceptionTranslationFilter`>> initiates __Start Authentication__.
  29. The configured <<servlet-authentication-authenticationentrypoint,`AuthenticationEntryPoint`>> is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`] which redirects to <<servlet-saml2login-sp-initiated-factory,the `<saml2:AuthnRequest>` generating endpoint>>, `Saml2WebSsoAuthenticationRequestFilter`.
  30. Or, if you've <<servlet-saml2login-relyingpartyregistrationrepository,configured more than one asserting party>>, it will first redirect to a picker page.
  31. 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`>>.
  32. image:{icondir}/number_5.png[] Then, the browser takes this `<saml2:AuthnRequest>` and presents it to the asserting party.
  33. The asserting party attempts to authentication the user.
  34. If successful, it will return a `<saml2:Response>` back to the browser.
  35. image:{icondir}/number_6.png[] The browser then POSTs the `<saml2:Response>` to the assertion consumer service endpoint.
  36. [[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
  37. .Authenticating a `<saml2:Response>`
  38. image::{figures}/saml2webssoauthenticationfilter.png[]
  39. The figure builds off our <<servlet-securityfilterchain,`SecurityFilterChain`>> diagram.
  40. image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it <<servlet-saml2login-authenticate-responses, delegates to `Saml2WebSsoAuthenticationFilter`>>.
  41. This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
  42. This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
  43. image:{icondir}/number_2.png[] Next, the filter passes the token to its configured <<servlet-authentication-providermanager,`AuthenticationManager`>>.
  44. By default, it will use the <<servlet-saml2login-architecture,`OpenSAML authentication provider`>>.
  45. image:{icondir}/number_3.png[] If authentication fails, then __Failure__
  46. * The <<servlet-authentication-securitycontextholder, `SecurityContextHolder`>> is cleared out.
  47. * The <<servlet-authentication-authenticationentrypoint,`AuthenticationEntryPoint`>> is invoked to restart the authentication process.
  48. image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
  49. * The <<servlet-authentication-authentication, `Authentication`>> is set on the <<servlet-authentication-securitycontextholder, `SecurityContextHolder`>>.
  50. * The `Saml2WebSsoAuthenticationFilter` invokes `FilterChain#doFilter(request,response)` to continue with the rest of the application logic.
  51. [[servlet-saml2login-minimaldependencies]]
  52. === Minimal Dependencies
  53. SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`.
  54. It builds off of the OpenSAML library.
  55. [[servlet-saml2login-minimalconfiguration]]
  56. === Minimal Configuration
  57. When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a service provider consists of two basic steps.
  58. First, include the needed dependencies and second, indicate the necessary asserting party metadata.
  59. [NOTE]
  60. Also, this presupposes that you've already <<servlet-saml2login-metadata, registered the relying party with your asserting party>>.
  61. ==== Specifying Identity Provider Metadata
  62. In a Spring Boot application, to specify an identity provider's metadata, simply do:
  63. [source,yml]
  64. ----
  65. spring:
  66. security:
  67. saml2:
  68. relyingparty:
  69. registration:
  70. adfs:
  71. identityprovider:
  72. entity-id: https://idp.example.com/issuer
  73. verification.credentials:
  74. - certificate-location: "classpath:idp.crt"
  75. singlesignon.url: https://idp.example.com/issuer/sso
  76. singlesignon.sign-request: false
  77. ----
  78. where
  79. * `https://idp.example.com/issuer` is the value contained in the `Issuer` attribute of the SAML responses that the identity provider will issue
  80. * `classpath:idp.crt` is the location on the classpath for the identity provider's certificate for verifying SAML responses, and
  81. * `https://idp.example.com/issuer/sso` is the endpoint where the identity provider is expecting `AuthnRequest` s.
  82. And that's it!
  83. [NOTE]
  84. Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party.
  85. These are frequently abbreviated as AP and RP, respectively.
  86. ==== Runtime Expectations
  87. As configured above, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
  88. [source,html]
  89. ----
  90. POST /login/saml2/sso/adfs HTTP/1.1
  91. SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
  92. ----
  93. There are two ways to see induce your asserting party to generate a `SAMLResponse`:
  94. * First, you can navigate to your asserting party.
  95. It likely has some kind of link or button for each registered relying party that you can click to send the `SAMLResponse`.
  96. * Second, you can navigate to a protected page in your app, for example, `http://localhost:8080`.
  97. Your app then redirects to the configured asserting party which then sends the `SAMLResponse`.
  98. From here, consider jumping to:
  99. * <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
  100. * <<servlet-saml2login-authenticatedprincipal,How to Use the `Saml2AuthenticatedPrincipal`>>
  101. * <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
  102. [[servlet-saml2login-architecture]]
  103. === How SAML 2.0 Login Integrates with OpenSAML
  104. Spring Security's SAML 2.0 support has a couple of design goals:
  105. * First, rely on a library for SAML 2.0 operations and domain objects.
  106. To achieve this, Spring Security uses OpenSAML.
  107. * Second, ensure this library is not required when using Spring Security's SAML support.
  108. To achieve this, any interfaces or classes where Spring Security uses OpenSAML in the contract remain encapsulated.
  109. This makes it possible for you to switch out OpenSAML for some other library or even an unsupported version of OpenSAML.
  110. As a natural outcome of the above two goals, Spring Security's SAML API is quite small relative to other modules.
  111. Instead, classes like `OpenSaml4AuthenticationRequestFactory` and `OpenSaml4AuthenticationProvider` expose `Converter` s that customize various steps in the authentication process.
  112. For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter will delegate to `OpenSaml4AuthenticationProvider`.
  113. [NOTE]
  114. For backward compatibility, Spring Security will use the latest OpenSAML 3 by default.
  115. Note, though that OpenSAML 3 has reached it's end-of-life and updating to OpenSAML 4.x is recommended.
  116. For that reason, Spring Security supports both OpenSAML 3.x and 4.x.
  117. If you manage your OpenSAML dependency to 4.x, then Spring Security will select its OpenSAML 4.x implementations.
  118. .Authenticating an OpenSAML `Response`
  119. image:{figures}/opensamlauthenticationprovider.png[]
  120. This figure builds off of the <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter` diagram>>.
  121. image:{icondir}/number_1.png[] The `Saml2WebSsoAuthenticationFilter` formulates the `Saml2AuthenticationToken` and invokes the <<servlet-authentication-providermanager,`AuthenticationManager`>>.
  122. image:{icondir}/number_2.png[] The <<servlet-authentication-providermanager,`AuthenticationManager`>> invokes the OpenSAML authentication provider.
  123. image:{icondir}/number_3.png[] The authentication provider deserializes the response into an OpenSAML `Response` and checks its signature.
  124. If the signature is invalid, authentication fails.
  125. image:{icondir}/number_4.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedAssertion` elements>>.
  126. If any decryptions fail, authentication fails.
  127. image:{icondir}/number_5.png[] Next, the provider validates the response's `Issuer` and `Destination` values.
  128. If they don't match what's in the `RelyingPartyRegistration`, authentication fails.
  129. image:{icondir}/number_6.png[] After that, the provider verifies the signature of each `Assertion`.
  130. If any signature is invalid, authentication fails.
  131. Also, if neither the response nor the assertions have signatures, authentication fails.
  132. Either the response or all the assertions must have signatures.
  133. image:{icondir}/number_7.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedID` or `EncryptedAttribute` elements>>.
  134. If any decryptions fail, authentication fails.
  135. image:{icondir}/number_8.png[] Next, the provider validates each assertion's `ExpiresAt` and `NotBefore` timestamps, the `<Subject>` and any `<AudienceRestriction>` conditions.
  136. If any validations fail, authentication fails.
  137. image:{icondir}/number_9.png[] Following that, the provider takes the first assertion's `AttributeStatement` and maps it to a `Map<String, List<Object>>`.
  138. It also grants the `ROLE_USER` granted authority.
  139. 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`.
  140. Then, it places that principal and the authorities into a `Saml2Authentication`.
  141. The resulting `Authentication#getPrincipal` is a Spring Security `Saml2AuthenticatedPrincipal` object, and `Authentication#getName` maps to the first assertion's `NameID` element.
  142. [[servlet-saml2login-opensaml-customization]]
  143. ==== Customizing OpenSAML Configuration
  144. Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class, like so:
  145. ====
  146. .Java
  147. [source,java,role="primary"]
  148. ----
  149. static {
  150. OpenSamlInitializationService.initialize();
  151. }
  152. ----
  153. .Kotlin
  154. [source,kotlin,role="secondary"]
  155. ----
  156. companion object {
  157. init {
  158. OpenSamlInitializationService.initialize()
  159. }
  160. }
  161. ----
  162. ====
  163. This replaces OpenSAML's `InitializationService#initialize`.
  164. Occasionally, it can be valuable to customize how OpenSAML builds, marshalls, and unmarshalls SAML objects.
  165. In these circumstances, you may instead want to call `OpenSamlInitializationService#requireInitialize(Consumer)` that gives you access to OpenSAML's `XMLObjectProviderFactory`.
  166. For example, when sending an unsigned AuthNRequest, you may want to force reauthentication.
  167. In that case, you can register your own `AuthnRequestMarshaller`, like so:
  168. ====
  169. .Java
  170. [source,java,role="primary"]
  171. ----
  172. static {
  173. OpenSamlInitializationService.requireInitialize(factory -> {
  174. AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
  175. @Override
  176. public Element marshall(XMLObject object, Element element) throws MarshallingException {
  177. configureAuthnRequest((AuthnRequest) object);
  178. return super.marshall(object, element);
  179. }
  180. public Element marshall(XMLObject object, Document document) throws MarshallingException {
  181. configureAuthnRequest((AuthnRequest) object);
  182. return super.marshall(object, document);
  183. }
  184. private void configureAuthnRequest(AuthnRequest authnRequest) {
  185. authnRequest.setForceAuthn(true);
  186. }
  187. }
  188. factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
  189. });
  190. }
  191. ----
  192. .Kotlin
  193. [source,kotlin,role="secondary"]
  194. ----
  195. companion object {
  196. init {
  197. OpenSamlInitializationService.requireInitialize {
  198. val marshaller = object : AuthnRequestMarshaller() {
  199. override fun marshall(xmlObject: XMLObject, element: Element): Element {
  200. configureAuthnRequest(xmlObject as AuthnRequest)
  201. return super.marshall(xmlObject, element)
  202. }
  203. override fun marshall(xmlObject: XMLObject, document: Document): Element {
  204. configureAuthnRequest(xmlObject as AuthnRequest)
  205. return super.marshall(xmlObject, document)
  206. }
  207. private fun configureAuthnRequest(authnRequest: AuthnRequest) {
  208. authnRequest.isForceAuthn = true
  209. }
  210. }
  211. it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
  212. }
  213. }
  214. }
  215. ----
  216. ====
  217. The `requireInitialize` method may only be called once per application instance.
  218. [[servlet-saml2login-sansboot]]
  219. === Overriding or Replacing Boot Auto Configuration
  220. There are two `@Bean` s that Spring Boot generates for a relying party.
  221. The first is a `WebSecurityConfigurerAdapter` that configures the app as a relying party.
  222. When including `spring-security-saml2-service-provider`, the `WebSecurityConfigurerAdapter` looks like:
  223. .Default JWT Configuration
  224. ====
  225. .Java
  226. [source,java,role="primary"]
  227. ----
  228. protected void configure(HttpSecurity http) {
  229. http
  230. .authorizeRequests(authorize -> authorize
  231. .anyRequest().authenticated()
  232. )
  233. .saml2Login(withDefaults());
  234. }
  235. ----
  236. .Kotlin
  237. [source,kotlin,role="secondary"]
  238. ----
  239. fun configure(http: HttpSecurity) {
  240. http {
  241. authorizeRequests {
  242. authorize(anyRequest, authenticated)
  243. }
  244. saml2Login { }
  245. }
  246. }
  247. ----
  248. ====
  249. If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one.
  250. You can replace this by exposing the bean within the application:
  251. .Custom SAML 2.0 Login Configuration
  252. ====
  253. .Java
  254. [source,java,role="primary"]
  255. ----
  256. @EnableWebSecurity
  257. public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
  258. protected void configure(HttpSecurity http) {
  259. http
  260. .authorizeRequests(authorize -> authorize
  261. .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
  262. .anyRequest().authenticated()
  263. )
  264. .saml2Login(withDefaults());
  265. }
  266. }
  267. ----
  268. .Kotlin
  269. [source,kotlin,role="secondary"]
  270. ----
  271. @EnableWebSecurity
  272. class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
  273. override fun configure(http: HttpSecurity) {
  274. http {
  275. authorizeRequests {
  276. authorize("/messages/**", hasAuthority("ROLE_USER"))
  277. authorize(anyRequest, authenticated)
  278. }
  279. saml2Login {
  280. }
  281. }
  282. }
  283. }
  284. ----
  285. ====
  286. The above requires the role of `USER` for any URL that starts with `/messages/`.
  287. [[servlet-saml2login-relyingpartyregistrationrepository]]
  288. 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.
  289. This includes things like the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.
  290. You can override the default by publishing your own `RelyingPartyRegistrationRepository` bean.
  291. For example, you can look up the asserting party's configuration by hitting its metadata endpoint like so:
  292. .Relying Party Registration Repository
  293. ====
  294. .Java
  295. [source,java,role="primary"]
  296. ----
  297. @Value("${metadata.location}")
  298. String assertingPartyMetadataLocation;
  299. @Bean
  300. public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
  301. RelyingPartyRegistration registration = RelyingPartyRegistrations
  302. .fromMetadataLocation(assertingPartyMetadataLocation)
  303. .registrationId("example")
  304. .build();
  305. return new InMemoryRelyingPartyRegistrationRepository(registration);
  306. }
  307. ----
  308. .Kotlin
  309. [source,kotlin,role="secondary"]
  310. ----
  311. @Value("\${metadata.location}")
  312. var assertingPartyMetadataLocation: String? = null
  313. @Bean
  314. open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
  315. val registration = RelyingPartyRegistrations
  316. .fromMetadataLocation(assertingPartyMetadataLocation)
  317. .registrationId("example")
  318. .build()
  319. return InMemoryRelyingPartyRegistrationRepository(registration)
  320. }
  321. ----
  322. ====
  323. Or you can provide each detail manually, as you can see below:
  324. .Relying Party Registration Repository Manual Configuration
  325. ====
  326. .Java
  327. [source,java,role="primary"]
  328. ----
  329. @Value("${verification.key}")
  330. File verificationKey;
  331. @Bean
  332. public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
  333. X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
  334. Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
  335. RelyingPartyRegistration registration = RelyingPartyRegistration
  336. .withRegistrationId("example")
  337. .assertingPartyDetails(party -> party
  338. .entityId("https://idp.example.com/issuer")
  339. .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
  340. .wantAuthnRequestsSigned(false)
  341. .verificationX509Credentials(c -> c.add(credential))
  342. )
  343. .build();
  344. return new InMemoryRelyingPartyRegistrationRepository(registration);
  345. }
  346. ----
  347. .Kotlin
  348. [source,kotlin,role="secondary"]
  349. ----
  350. @Value("\${verification.key}")
  351. var verificationKey: File? = null
  352. @Bean
  353. open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
  354. val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
  355. val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
  356. val registration = RelyingPartyRegistration
  357. .withRegistrationId("example")
  358. .assertingPartyDetails { party: AssertingPartyDetails.Builder ->
  359. party
  360. .entityId("https://idp.example.com/issuer")
  361. .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
  362. .wantAuthnRequestsSigned(false)
  363. .verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
  364. c.add(
  365. credential
  366. )
  367. }
  368. }
  369. .build()
  370. return InMemoryRelyingPartyRegistrationRepository(registration)
  371. }
  372. ----
  373. ====
  374. [NOTE]
  375. Note that `X509Support` is an OpenSAML class, used here in the snippet for brevity
  376. [[servlet-saml2login-relyingpartyregistrationrepository-dsl]]
  377. Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configured `WebSecurityConfigurerAdapter`:
  378. .Custom Relying Party Registration DSL
  379. ====
  380. .Java
  381. [source,java,role="primary"]
  382. ----
  383. @EnableWebSecurity
  384. public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
  385. protected void configure(HttpSecurity http) {
  386. http
  387. .authorizeRequests(authorize -> authorize
  388. .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
  389. .anyRequest().authenticated()
  390. )
  391. .saml2Login(saml2 -> saml2
  392. .relyingPartyRegistrationRepository(relyingPartyRegistrations())
  393. );
  394. }
  395. }
  396. ----
  397. .Kotlin
  398. [source,kotlin,role="secondary"]
  399. ----
  400. @EnableWebSecurity
  401. class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
  402. override fun configure(http: HttpSecurity) {
  403. http {
  404. authorizeRequests {
  405. authorize("/messages/**", hasAuthority("ROLE_USER"))
  406. authorize(anyRequest, authenticated)
  407. }
  408. saml2Login {
  409. relyingPartyRegistrationRepository = relyingPartyRegistrations()
  410. }
  411. }
  412. }
  413. }
  414. ----
  415. ====
  416. [NOTE]
  417. A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`.
  418. [[servlet-saml2login-relyingpartyregistration]]
  419. === RelyingPartyRegistration
  420. A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
  421. instance represents a link between an relying party and assering party's metadata.
  422. 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.
  423. 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.
  424. The following `RelyingPartyRegistration` is the minimum required for most setups:
  425. ====
  426. .Java
  427. [source,java,role="primary"]
  428. ----
  429. RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
  430. .fromMetadataLocation("https://ap.example.org/metadata")
  431. .registrationId("my-id")
  432. .build();
  433. ----
  434. .Kotlin
  435. [source,kotlin,role="secondary"]
  436. ----
  437. val relyingPartyRegistration = RelyingPartyRegistrations
  438. .fromMetadataLocation("https://ap.example.org/metadata")
  439. .registrationId("my-id")
  440. .build()
  441. ----
  442. ====
  443. Note that you can also create a `RelyingPartyRegistration` from an arbitrary `InputStream` source.
  444. One such example is when the metadata is stored in a database:
  445. [source,java]
  446. ----
  447. String xml = fromDatabase();
  448. try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
  449. RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
  450. .fromMetadata(source)
  451. .registrationId("my-id")
  452. .build();
  453. }
  454. ----
  455. Though a more sophisticated setup is also possible, like so:
  456. ====
  457. .Java
  458. [source,java,role="primary"]
  459. ----
  460. RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
  461. .entityId("{baseUrl}/{registrationId}")
  462. .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
  463. .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
  464. .assertingPartyDetails(party -> party
  465. .entityId("https://ap.example.org")
  466. .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
  467. .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
  468. )
  469. .build();
  470. ----
  471. .Kotlin
  472. [source,kotlin,role="secondary"]
  473. ----
  474. val relyingPartyRegistration =
  475. RelyingPartyRegistration.withRegistrationId("my-id")
  476. .entityId("{baseUrl}/{registrationId}")
  477. .decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
  478. c.add(relyingPartyDecryptingCredential())
  479. }
  480. .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
  481. .assertingPartyDetails { party -> party
  482. .entityId("https://ap.example.org")
  483. .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
  484. .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
  485. }
  486. .build()
  487. ----
  488. ====
  489. [TIP]
  490. The top-level metadata methods are details about the relying party.
  491. The methods inside `assertingPartyDetails` are details about the asserting party.
  492. [NOTE]
  493. The location where a relying party is expecting SAML Responses is the Assertion Consumer Service Location.
  494. The default for the relying party's `entityId` is `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`.
  495. This is this value needed when configuring the asserting party to know about your relying party.
  496. The default for the `assertionConsumerServiceLocation` is `+/login/saml2/sso/{registrationId}+`.
  497. It's mapped by default to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
  498. [[servlet-saml2login-rpr-uripatterns]]
  499. ==== URI Patterns
  500. You probably noticed in the above examples the `+{baseUrl}+` and `+{registrationId}+` placeholders.
  501. These are useful for generating URIs. As such, the relying party's `entityId` and `assertionConsumerServiceLocation` support the following placeholders:
  502. * `baseUrl` - the scheme, host, and port of a deployed application
  503. * `registrationId` - the registration id for this relying party
  504. * `baseScheme` - the scheme of a deployed application
  505. * `baseHost` - the host of a deployed application
  506. * `basePort` - the port of a deployed application
  507. For example, the `assertionConsumerServiceLocation` defined above was:
  508. `+/my-login-endpoint/{registrationId}+`
  509. which in a deployed application would translate to
  510. `+/my-login-endpoint/adfs+`
  511. The `entityId` above was defined as:
  512. `+{baseUrl}/{registrationId}+`
  513. which in a deployed application would translate to
  514. `+https://rp.example.com/adfs+`
  515. [[servlet-saml2login-rpr-credentials]]
  516. ==== Credentials
  517. You also likely noticed the credential that was used.
  518. Oftentimes, a relying party will use the same key to sign payloads as well as decrypt them.
  519. Or it will use the same key to verify payloads as well as encrypt them.
  520. Because of this, Spring Security ships with `Saml2X509Credential`, a SAML-specific credential that simplifies configuring the same key for different use cases.
  521. 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.
  522. To construct a `Saml2X509Credential` that you'll use to verify assertions from the asserting party, you can load the file and use
  523. the `CertificateFactory` like so:
  524. ====
  525. .Java
  526. [source,java,role="primary"]
  527. ----
  528. Resource resource = new ClassPathResource("ap.crt");
  529. try (InputStream is = resource.getInputStream()) {
  530. X509Certificate certificate = (X509Certificate)
  531. CertificateFactory.getInstance("X.509").generateCertificate(is);
  532. return Saml2X509Credential.verification(certificate);
  533. }
  534. ----
  535. .Kotlin
  536. [source,kotlin,role="secondary"]
  537. ----
  538. val resource = ClassPathResource("ap.crt")
  539. resource.inputStream.use {
  540. return Saml2X509Credential.verification(
  541. CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
  542. )
  543. }
  544. ----
  545. ====
  546. Let's say that the asserting party is going to also encrypt the assertion.
  547. In that case, the relying party will need a private key to be able to decrypt the encrypted value.
  548. In that case, you'll need an `RSAPrivateKey` as well as its corresponding `X509Certificate`.
  549. You can load the first using Spring Security's `RsaKeyConverters` utility class and the second as you did before:
  550. ====
  551. .Java
  552. [source,java,role="primary"]
  553. ----
  554. X509Certificate certificate = relyingPartyDecryptionCertificate();
  555. Resource resource = new ClassPathResource("rp.crt");
  556. try (InputStream is = resource.getInputStream()) {
  557. RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
  558. return Saml2X509Credential.decryption(rsa, certificate);
  559. }
  560. ----
  561. .Kotlin
  562. [source,kotlin,role="secondary"]
  563. ----
  564. val certificate: X509Certificate = relyingPartyDecryptionCertificate()
  565. val resource = ClassPathResource("rp.crt")
  566. resource.inputStream.use {
  567. val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
  568. return Saml2X509Credential.decryption(rsa, certificate)
  569. }
  570. ----
  571. ====
  572. [TIP]
  573. When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
  574. [[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
  575. ==== Resolving the Relying Party from the Request
  576. As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
  577. There are a number of reasons you may want to customize. Among them:
  578. * You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme
  579. * You may identify tenants in a way other than by the URI path
  580. To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `Converter<HttpServletRequest, RelyingPartyRegistration>`.
  581. The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
  582. You can provide a simpler resolver that, for example, always returns the same relying party:
  583. ====
  584. .Java
  585. [source,java,role="primary"]
  586. ----
  587. public class SingleRelyingPartyRegistrationResolver
  588. implements Converter<HttpServletRequest, RelyingPartyRegistration> {
  589. @Override
  590. public RelyingPartyRegistration convert(HttpServletRequest request) {
  591. return this.relyingParty;
  592. }
  593. }
  594. ----
  595. .Kotlin
  596. [source,kotlin,role="secondary"]
  597. ----
  598. class SingleRelyingPartyRegistrationResolver : Converter<HttpServletRequest?, RelyingPartyRegistration?> {
  599. override fun convert(request: HttpServletRequest?): RelyingPartyRegistration? {
  600. return this.relyingParty
  601. }
  602. }
  603. ----
  604. ====
  605. Then, you can provide this resolver to the appropriate filters that <<servlet-saml2login-sp-initiated-factory, produce `<saml2:AuthnRequest>` s>>, <<servlet-saml2login-authenticate-responses, authenticate `<saml2:Response>` s>>, and <<servlet-saml2login-metadata, produce `<saml2:SPSSODescriptor>` metadata>>.
  606. [NOTE]
  607. Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
  608. [[servlet-saml2login-rpr-duplicated]]
  609. ==== Duplicated Relying Party Configurations
  610. When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances:
  611. * The relying party's `entityId`
  612. * Its `assertionConsumerServiceLocation`, and
  613. * Its credentials, for example its signing or decryption credentials
  614. What's nice about this setup is credentials may be more easily rotated for some identity providers vs others.
  615. The duplication can be alleviated in a few different ways.
  616. First, in YAML this can be alleviated with references, like so:
  617. [source,yaml]
  618. ----
  619. spring:
  620. security:
  621. saml2:
  622. relyingparty:
  623. okta:
  624. signing.credentials: &relying-party-credentials
  625. - private-key-location: classpath:rp.key
  626. - certificate-location: classpath:rp.crt
  627. identityprovider:
  628. entity-id: ...
  629. azure:
  630. signing.credentials: *relying-party-credentials
  631. identityprovider:
  632. entity-id: ...
  633. ----
  634. Second, in a database, it's not necessary to replicate `RelyingPartyRegistration` 's model.
  635. Third, in Java, you can create a custom configuration method, like so:
  636. ====
  637. .Java
  638. [source,java,role="primary"]
  639. ----
  640. private RelyingPartyRegistration.Builder
  641. addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {
  642. Saml2X509Credential signingCredential = ...
  643. builder.signingX509Credentials(c -> c.addAll(signingCredential));
  644. // ... other relying party configurations
  645. }
  646. @Bean
  647. public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
  648. RelyingPartyRegistration okta = addRelyingPartyDetails(
  649. RelyingPartyRegistrations
  650. .fromMetadataLocation(oktaMetadataUrl)
  651. .registrationId("okta")).build();
  652. RelyingPartyRegistration azure = addRelyingPartyDetails(
  653. RelyingPartyRegistrations
  654. .fromMetadataLocation(oktaMetadataUrl)
  655. .registrationId("azure")).build();
  656. return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
  657. }
  658. ----
  659. .Kotlin
  660. [source,kotlin,role="secondary"]
  661. ----
  662. private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
  663. val signingCredential: Saml2X509Credential = ...
  664. builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
  665. c.add(
  666. signingCredential
  667. )
  668. }
  669. // ... other relying party configurations
  670. }
  671. @Bean
  672. open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
  673. val okta = addRelyingPartyDetails(
  674. RelyingPartyRegistrations
  675. .fromMetadataLocation(oktaMetadataUrl)
  676. .registrationId("okta")
  677. ).build()
  678. val azure = addRelyingPartyDetails(
  679. RelyingPartyRegistrations
  680. .fromMetadataLocation(oktaMetadataUrl)
  681. .registrationId("azure")
  682. ).build()
  683. return InMemoryRelyingPartyRegistrationRepository(okta, azure)
  684. }
  685. ----
  686. ====
  687. [[servlet-saml2login-sp-initiated-factory]]
  688. === Producing `<saml2:AuthnRequest>` s
  689. As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party.
  690. Spring Security achieves this in part by registering the `Saml2WebSsoAuthenticationRequestFilter` in the filter chain.
  691. This filter by default responds to endpoint `+/saml2/authenticate/{registrationId}+`.
  692. For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to:
  693. `https://rp.example.org/saml2/authenticate/ping`
  694. and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
  695. [[servlet-saml2login-sp-initiated-factory-signing]]
  696. ==== Changing How the `<saml2:AuthnRequest>` Gets Sent
  697. By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party.
  698. Many asserting parties don't require a signed `<saml2:AuthnRequest>`.
  699. This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
  700. .Not Requiring Signed AuthnRequests
  701. ====
  702. .Boot
  703. [source,yaml,role="primary"]
  704. ----
  705. spring:
  706. security:
  707. saml2:
  708. relyingparty:
  709. okta:
  710. identityprovider:
  711. entity-id: ...
  712. singlesignon.sign-request: false
  713. ----
  714. .Java
  715. [source,java,role="secondary"]
  716. ----
  717. RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
  718. // ...
  719. .assertingPartyDetails(party -> party
  720. // ...
  721. .wantAuthnRequestsSigned(false)
  722. )
  723. .build();
  724. ----
  725. .Kotlin
  726. [source,java,role="secondary"]
  727. ----
  728. var relyingPartyRegistration: RelyingPartyRegistration =
  729. RelyingPartyRegistration.withRegistrationId("okta")
  730. // ...
  731. .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
  732. // ...
  733. .wantAuthnRequestsSigned(false)
  734. }
  735. .build();
  736. ----
  737. ====
  738. Otherwise, you will need to specify a private key to `RelyingPartyRegistration#signingX509Credentials` so that Spring Security can sign the `<saml2:AuthnRequest>` before sending.
  739. [[servlet-saml2login-sp-initiated-factory-algorithm]]
  740. 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.
  741. You can configure the algorithm based on the asserting party's <<servlet-saml2login-relyingpartyregistrationrepository,metadata using `RelyingPartyRegistrations`>>.
  742. Or, you can provide it manually:
  743. ====
  744. .Java
  745. [source,java,role="primary"]
  746. ----
  747. String metadataLocation = "classpath:asserting-party-metadata.xml";
  748. RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
  749. // ...
  750. .assertingPartyDetails((party) -> party
  751. // ...
  752. .signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
  753. )
  754. .build();
  755. ----
  756. .Kotlin
  757. [source,kotlin,role="secondary"]
  758. ----
  759. var metadataLocation = "classpath:asserting-party-metadata.xml"
  760. var relyingPartyRegistration: RelyingPartyRegistration =
  761. RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
  762. // ...
  763. .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
  764. // ...
  765. .signingAlgorithms { sign: MutableList<String?> ->
  766. sign.add(
  767. SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512
  768. )
  769. }
  770. }
  771. .build();
  772. ----
  773. ====
  774. NOTE: The snippet above uses the OpenSAML `SignatureConstants` class to supply the algorithm name.
  775. But, that's just for convenience.
  776. Since the datatype is `String`, you can supply the name of the algorithm directly.
  777. [[servlet-saml2login-sp-initiated-factory-binding]]
  778. Some asserting parties require that the `<saml2:AuthnRequest>` be POSTed.
  779. This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
  780. ====
  781. .Java
  782. [source,java,role="primary"]
  783. ----
  784. RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
  785. // ...
  786. .assertingPartyDetails(party -> party
  787. // ...
  788. .singleSignOnServiceBinding(Saml2MessageBinding.POST)
  789. )
  790. .build();
  791. ----
  792. .Kotlin
  793. [source,kotlin,role="secondary"]
  794. ----
  795. var relyingPartyRegistration: RelyingPartyRegistration? =
  796. RelyingPartyRegistration.withRegistrationId("okta")
  797. // ...
  798. .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
  799. // ...
  800. .singleSignOnServiceBinding(Saml2MessageBinding.POST)
  801. }
  802. .build()
  803. ----
  804. ====
  805. [[servlet-saml2login-sp-initiated-factory-custom-authnrequest]]
  806. ==== Customizing OpenSAML's `AuthnRequest` Instance
  807. There are a number of reasons that you may want to adjust an `AuthnRequest`.
  808. For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
  809. 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>>.
  810. This will give you access to post-process the `AuthnRequest` instance before it's serialized.
  811. 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:
  812. ====
  813. .Java
  814. [source,java,role="primary"]
  815. ----
  816. @Component
  817. public class AuthnRequestConverter implements
  818. Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
  819. private final AuthnRequestBuilder authnRequestBuilder;
  820. private final IssuerBuilder issuerBuilder;
  821. // ... constructor
  822. public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
  823. MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context;
  824. Issuer issuer = issuerBuilder.buildObject();
  825. issuer.setValue(myContext.getIssuer());
  826. AuthnRequest authnRequest = authnRequestBuilder.buildObject();
  827. authnRequest.setIssuer(issuer);
  828. authnRequest.setDestination(myContext.getDestination());
  829. authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl());
  830. // ... additional settings
  831. authRequest.setForceAuthn(myContext.getForceAuthn());
  832. return authnRequest;
  833. }
  834. }
  835. ----
  836. .Kotlin
  837. [source,kotlin,role="secondary"]
  838. ----
  839. @Component
  840. class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
  841. private val authnRequestBuilder: AuthnRequestBuilder? = null
  842. private val issuerBuilder: IssuerBuilder? = null
  843. // ... constructor
  844. override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
  845. val myContext: MySaml2AuthenticationRequestContext = context
  846. val issuer: Issuer = issuerBuilder.buildObject()
  847. issuer.value = myContext.getIssuer()
  848. val authnRequest: AuthnRequest = authnRequestBuilder.buildObject()
  849. authnRequest.issuer = issuer
  850. authnRequest.destination = myContext.getDestination()
  851. authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl()
  852. // ... additional settings
  853. authRequest.setForceAuthn(myContext.getForceAuthn())
  854. return authnRequest
  855. }
  856. }
  857. ----
  858. ====
  859. Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as `@Bean` s:
  860. ====
  861. .Java
  862. [source,java,role="primary"]
  863. ----
  864. @Bean
  865. Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() {
  866. Saml2AuthenticationRequestContextResolver resolver =
  867. new DefaultSaml2AuthenticationRequestContextResolver();
  868. return request -> {
  869. Saml2AuthenticationRequestContext context = resolver.resolve(request);
  870. return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null);
  871. };
  872. }
  873. @Bean
  874. Saml2AuthenticationRequestFactory authenticationRequestFactory(
  875. AuthnRequestConverter authnRequestConverter) {
  876. OpenSaml4AuthenticationRequestFactory authenticationRequestFactory =
  877. new OpenSaml4AuthenticationRequestFactory();
  878. authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
  879. return authenticationRequestFactory;
  880. }
  881. ----
  882. .Kotlin
  883. [source,kotlin,role="secondary"]
  884. ----
  885. @Bean
  886. open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver {
  887. val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver()
  888. return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest ->
  889. val context = resolver.resolve(request)
  890. MySaml2AuthenticationRequestContext(
  891. context,
  892. request.getParameter("force") != null
  893. )
  894. }
  895. }
  896. @Bean
  897. open fun authenticationRequestFactory(
  898. authnRequestConverter: AuthnRequestConverter?
  899. ): Saml2AuthenticationRequestFactory? {
  900. val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory()
  901. authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter)
  902. return authenticationRequestFactory
  903. }
  904. ----
  905. ====
  906. [[servlet-saml2login-authenticate-responses]]
  907. === Authenticating `<saml2:Response>` s
  908. To verify SAML 2.0 Responses, Spring Security uses <<servlet-saml2login-architecture,`OpenSaml4AuthenticationProvider`>> by default.
  909. You can configure this in a number of ways including:
  910. 1. Setting a clock skew to timestamp validation
  911. 2. Mapping the response to a list of `GrantedAuthority` instances
  912. 3. Customizing the strategy for validating assertions
  913. 4. Customizing the strategy for decrypting response and assertion elements
  914. To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
  915. [[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
  916. ==== Setting a Clock Skew
  917. It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized.
  918. For that reason, you can configure `OpenSaml4AuthenticationProvider` 's default assertion validator with some tolerance:
  919. ====
  920. .Java
  921. [source,java,role="primary"]
  922. ----
  923. @EnableWebSecurity
  924. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  925. @Override
  926. protected void configure(HttpSecurity http) throws Exception {
  927. OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
  928. authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
  929. .createDefaultAssertionValidator(assertionToken -> {
  930. Map<String, Object> params = new HashMap<>();
  931. params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
  932. // ... other validation parameters
  933. return new ValidationContext(params);
  934. })
  935. );
  936. http
  937. .authorizeRequests(authz -> authz
  938. .anyRequest().authenticated()
  939. )
  940. .saml2Login(saml2 -> saml2
  941. .authenticationManager(new ProviderManager(authenticationProvider))
  942. );
  943. }
  944. }
  945. ----
  946. .Kotlin
  947. [source,kotlin,role="secondary"]
  948. ----
  949. @EnableWebSecurity
  950. open class SecurityConfig : WebSecurityConfigurerAdapter() {
  951. override fun configure(http: HttpSecurity) {
  952. val authenticationProvider = OpenSaml4AuthenticationProvider()
  953. authenticationProvider.setAssertionValidator(
  954. OpenSaml4AuthenticationProvider
  955. .createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
  956. val params: MutableMap<String, Any> = HashMap()
  957. params[CLOCK_SKEW] =
  958. Duration.ofMinutes(10).toMillis()
  959. ValidationContext(params)
  960. })
  961. )
  962. http {
  963. authorizeRequests {
  964. authorize(anyRequest, authenticated)
  965. }
  966. saml2Login {
  967. authenticationManager = ProviderManager(authenticationProvider)
  968. }
  969. }
  970. }
  971. }
  972. ----
  973. ====
  974. [[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
  975. ==== Coordinating with a `UserDetailsService`
  976. Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
  977. In that case, the response authentication converter can come in handy, as can be seen below:
  978. ====
  979. .Java
  980. [source,java,role="primary"]
  981. ----
  982. @EnableWebSecurity
  983. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  984. @Autowired
  985. UserDetailsService userDetailsService;
  986. @Override
  987. protected void configure(HttpSecurity http) throws Exception {
  988. OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
  989. authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
  990. Saml2Authentication authentication = OpenSaml4AuthenticationProvider
  991. .createDefaultResponseAuthenticationConverter() <1>
  992. .convert(responseToken);
  993. Assertion assertion = responseToken.getResponse().getAssertions().get(0);
  994. String username = assertion.getSubject().getNameID().getValue();
  995. UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); <2>
  996. return MySaml2Authentication(userDetails, authentication); <3>
  997. });
  998. http
  999. .authorizeRequests(authz -> authz
  1000. .anyRequest().authenticated()
  1001. )
  1002. .saml2Login(saml2 -> saml2
  1003. .authenticationManager(new ProviderManager(authenticationProvider))
  1004. );
  1005. }
  1006. }
  1007. ----
  1008. .Kotlin
  1009. [source,kotlin,role="secondary"]
  1010. ----
  1011. @EnableWebSecurity
  1012. open class SecurityConfig : WebSecurityConfigurerAdapter() {
  1013. @Autowired
  1014. var userDetailsService: UserDetailsService? = null
  1015. override fun configure(http: HttpSecurity) {
  1016. val authenticationProvider = OpenSaml4AuthenticationProvider()
  1017. authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
  1018. val authentication = OpenSaml4AuthenticationProvider
  1019. .createDefaultResponseAuthenticationConverter() <1>
  1020. .convert(responseToken)
  1021. val assertion: Assertion = responseToken.response.assertions[0]
  1022. val username: String = assertion.subject.nameID.value
  1023. val userDetails = userDetailsService!!.loadUserByUsername(username) <2>
  1024. MySaml2Authentication(userDetails, authentication) <3>
  1025. }
  1026. http {
  1027. authorizeRequests {
  1028. authorize(anyRequest, authenticated)
  1029. }
  1030. saml2Login {
  1031. authenticationManager = ProviderManager(authenticationProvider)
  1032. }
  1033. }
  1034. }
  1035. }
  1036. ----
  1037. ====
  1038. <1> First, call the default converter, which extracts attributes and authorities from the response
  1039. <2> Second, call the <<servlet-authentication-userdetailsservice, `UserDetailsService`>> using the relevant information
  1040. <3> Third, return a custom authentication that includes the user details
  1041. [NOTE]
  1042. It's not required to call `OpenSaml4AuthenticationProvider` 's default authentication converter.
  1043. It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from `AttributeStatement` s as well as the single `ROLE_USER` authority.
  1044. [[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
  1045. ==== Performing Additional Response Validation
  1046. `OpenSaml4AuthenticationProvider` validates the `Issuer` and `Destination` values right after decrypting the `Response`.
  1047. You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.
  1048. For example, you can throw a custom exception with any additional information available in the `Response` object, like so:
  1049. [source,java]
  1050. ----
  1051. OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
  1052. provider.setResponseValidator((responseToken) -> {
  1053. Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
  1054. .createDefaultResponseValidator()
  1055. .convert(responseToken)
  1056. .concat(myCustomValidator.convert(responseToken));
  1057. if (!result.getErrors().isEmpty()) {
  1058. String inResponseTo = responseToken.getInResponseTo();
  1059. throw new CustomSaml2AuthenticationException(result, inResponseTo);
  1060. }
  1061. return result;
  1062. });
  1063. ----
  1064. ==== Performing Additional Assertion Validation
  1065. `OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
  1066. After verifying the signature, it will:
  1067. 1. Validate `<AudienceRestriction>` and `<DelegationRestriction>` conditions
  1068. 2. Validate `<SubjectConfirmation>` s, expect for any IP address information
  1069. To perform additional validation, you can configure your own assertion validator that delegates to `OpenSaml4AuthenticationProvider` 's default and then performs its own.
  1070. [[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]]
  1071. For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
  1072. ====
  1073. .Java
  1074. [source,java,role="primary"]
  1075. ----
  1076. OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
  1077. OneTimeUseConditionValidator validator = ...;
  1078. provider.setAssertionValidator(assertionToken -> {
  1079. Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
  1080. .createDefaultAssertionValidator()
  1081. .convert(assertionToken);
  1082. Assertion assertion = assertionToken.getAssertion();
  1083. OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
  1084. ValidationContext context = new ValidationContext();
  1085. try {
  1086. if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
  1087. return result;
  1088. }
  1089. } catch (Exception e) {
  1090. return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
  1091. }
  1092. return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
  1093. });
  1094. ----
  1095. .Kotlin
  1096. [source,kotlin,role="secondary"]
  1097. ----
  1098. var provider = OpenSaml4AuthenticationProvider()
  1099. var validator: OneTimeUseConditionValidator = ...
  1100. provider.setAssertionValidator { assertionToken ->
  1101. val result = OpenSaml4AuthenticationProvider
  1102. .createDefaultAssertionValidator()
  1103. .convert(assertionToken)
  1104. val assertion: Assertion = assertionToken.assertion
  1105. val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
  1106. val context = ValidationContext()
  1107. try {
  1108. if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
  1109. return@setAssertionValidator result
  1110. }
  1111. } catch (e: Exception) {
  1112. return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
  1113. }
  1114. result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
  1115. }
  1116. ----
  1117. ====
  1118. [NOTE]
  1119. While recommended, it's not necessary to call `OpenSaml4AuthenticationProvider` 's default assertion validator.
  1120. 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.
  1121. [[servlet-saml2login-opensamlauthenticationprovider-decryption]]
  1122. ==== Customizing Decryption
  1123. 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`>>.
  1124. `OpenSaml4AuthenticationProvider` exposes <<servlet-saml2login-architecture,two decryption strategies>>.
  1125. The response decrypter is for decrypting encrypted elements of the `<saml2:Response>`, like `<saml2:EncryptedAssertion>`.
  1126. The assertion decrypter is for decrypting encrypted elements of the `<saml2:Assertion>`, like `<saml2:EncryptedAttribute>` and `<saml2:EncryptedID>`.
  1127. You can replace `OpenSaml4AuthenticationProvider`'s default decryption strategy with your own.
  1128. For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>`, you can use it instead like so:
  1129. ====
  1130. .Java
  1131. [source,java,role="primary"]
  1132. ----
  1133. MyDecryptionService decryptionService = ...;
  1134. OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
  1135. provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
  1136. ----
  1137. .Kotlin
  1138. [source,kotlin,role="secondary"]
  1139. ----
  1140. val decryptionService: MyDecryptionService = ...
  1141. val provider = OpenSaml4AuthenticationProvider()
  1142. provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
  1143. ----
  1144. ====
  1145. If you are also decrypting individual elements in a `<saml2:Assertion>`, you can customize the assertion decrypter, too:
  1146. ====
  1147. .Java
  1148. [source,java,role="primary"]
  1149. ----
  1150. provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
  1151. ----
  1152. .Kotlin
  1153. [source,kotlin,role="secondary"]
  1154. ----
  1155. provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
  1156. ----
  1157. ====
  1158. NOTE: There are two separate decrypters since assertions can be signed separately from responses.
  1159. Trying to decrypt a signed assertion's elements before signature verification may invalidate the signature.
  1160. If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter.
  1161. [[servlet-saml2login-authenticationmanager-custom]]
  1162. ==== Using a Custom Authentication Manager
  1163. [[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]]
  1164. Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication.
  1165. This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2.0 Response XML data.
  1166. ====
  1167. .Java
  1168. [source,java,role="primary"]
  1169. ----
  1170. @EnableWebSecurity
  1171. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  1172. @Override
  1173. protected void configure(HttpSecurity http) throws Exception {
  1174. AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
  1175. http
  1176. .authorizeRequests(authorize -> authorize
  1177. .anyRequest().authenticated()
  1178. )
  1179. .saml2Login(saml2 -> saml2
  1180. .authenticationManager(authenticationManager)
  1181. )
  1182. ;
  1183. }
  1184. }
  1185. ----
  1186. .Kotlin
  1187. [source,kotlin,role="secondary"]
  1188. ----
  1189. @EnableWebSecurity
  1190. open class SecurityConfig : WebSecurityConfigurerAdapter() {
  1191. override fun configure(http: HttpSecurity) {
  1192. val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
  1193. http {
  1194. authorizeRequests {
  1195. authorize(anyRequest, authenticated)
  1196. }
  1197. saml2Login {
  1198. authenticationManager = customAuthenticationManager
  1199. }
  1200. }
  1201. }
  1202. }
  1203. ----
  1204. ====
  1205. [[servlet-saml2login-authenticatedprincipal]]
  1206. === Using `Saml2AuthenticatedPrincipal`
  1207. With the relying party correctly configured for a given asserting party, it's ready to accept assertions.
  1208. Once the relying party validates an assertion, the result is a `Saml2Authentication` with a `Saml2AuthenticatedPrincipal`.
  1209. This means that you can access the principal in your controller like so:
  1210. ====
  1211. .Java
  1212. [source,java,role="primary"]
  1213. ----
  1214. @Controller
  1215. public class MainController {
  1216. @GetMapping("/")
  1217. public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
  1218. String email = principal.getFirstAttribute("email");
  1219. model.setAttribute("email", email);
  1220. return "index";
  1221. }
  1222. }
  1223. ----
  1224. .Kotlin
  1225. [source,kotlin,role="secondary"]
  1226. ----
  1227. @Controller
  1228. class MainController {
  1229. @GetMapping("/")
  1230. fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
  1231. val email = principal.getFirstAttribute<String>("email")
  1232. model.setAttribute("email", email)
  1233. return "index"
  1234. }
  1235. }
  1236. ----
  1237. ====
  1238. [TIP]
  1239. 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.
  1240. `getFirstAttribute` is quite handy when you know that there is only one value.
  1241. [[servlet-saml2login-metadata]]
  1242. === Producing `<saml2:SPSSODescriptor>` Metadata
  1243. You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
  1244. ====
  1245. .Java
  1246. [source,java,role="primary"]
  1247. ----
  1248. Converter<HttpServletRequest, RelyingPartyRegistration> relyingPartyRegistrationResolver =
  1249. new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
  1250. Saml2MetadataFilter filter = new Saml2MetadataFilter(
  1251. relyingPartyRegistrationResolver,
  1252. new OpenSamlMetadataResolver());
  1253. http
  1254. // ...
  1255. .saml2Login(withDefaults())
  1256. .addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
  1257. ----
  1258. .Kotlin
  1259. [source,kotlin,role="secondary"]
  1260. ----
  1261. val relyingPartyRegistrationResolver: Converter<HttpServletRequest, RelyingPartyRegistration> =
  1262. DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)
  1263. val filter = Saml2MetadataFilter(
  1264. relyingPartyRegistrationResolver,
  1265. OpenSamlMetadataResolver()
  1266. )
  1267. http {
  1268. //...
  1269. saml2Login { }
  1270. addFilterBefore<Saml2WebSsoAuthenticationFilter>(filter)
  1271. }
  1272. ----
  1273. ====
  1274. You can use this metadata endpoint to register your relying party with your asserting party.
  1275. This is often as simple as finding the correct form field to supply the metadata endpoint.
  1276. By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`.
  1277. You can change this by calling the `setRequestMatcher` method on the filter:
  1278. ====
  1279. .Java
  1280. [source,java,role="primary"]
  1281. ----
  1282. filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
  1283. ----
  1284. .Kotlin
  1285. [source,kotlin,role="secondary"]
  1286. ----
  1287. filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"))
  1288. ----
  1289. ====
  1290. ensuring that the `registrationId` hint is at the end of the path.
  1291. Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a `registrationId` hint, like so:
  1292. ====
  1293. .Java
  1294. [source,java,role="primary"]
  1295. ----
  1296. filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
  1297. ----
  1298. .Kotlin
  1299. [source,kotlin,role="secondary"]
  1300. ----
  1301. filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
  1302. ----
  1303. ====
  1304. [[servlet-saml2login-logout]]
  1305. === Performing Single Logout
  1306. Spring Security does not yet support single logout.
  1307. Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
  1308. ====
  1309. .Java
  1310. [source,java,role="primary"]
  1311. ----
  1312. http
  1313. // ...
  1314. .logout(logout -> logout
  1315. .logoutSuccessHandler(myCustomSuccessHandler())
  1316. .logoutRequestMatcher(myRequestMatcher())
  1317. )
  1318. ----
  1319. .Kotlin
  1320. [source,kotlin,role="secondary"]
  1321. ----
  1322. http {
  1323. logout {
  1324. // ...
  1325. logoutSuccessHandler = myCustomSuccessHandler()
  1326. logoutRequestMatcher = myRequestMatcher()
  1327. }
  1328. }
  1329. ----
  1330. ====
  1331. The success handler will send logout requests to the asserting party.
  1332. The request matcher will detect logout requests from the asserting party.