overview.adoc 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  1. = SAML 2.0 Login Overview
  2. :figures: servlet/saml2
  3. :icondir: icons
  4. We start by examining how SAML 2.0 Relying Party Authentication works within Spring Security.
  5. First, we see that, like <<oauth2login, OAuth 2.0 Login>>, Spring Security takes the user to a third party for performing authentication.
  6. It does this through a series of redirects:
  7. .Redirecting to Asserting Party Authentication
  8. image::{figures}/saml2webssoauthenticationrequestfilter.png[]
  9. [NOTE]
  10. ====
  11. The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] and xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[`AbstractAuthenticationProcessingFilter`] diagrams:
  12. ====
  13. image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the `/private` resource, for which it is not authorized.
  14. 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`.
  15. image:{icondir}/number_3.png[] Since the user lacks authorization, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates _Start Authentication_.
  16. 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`.
  17. Alternatively, if you have <<servlet-saml2login-relyingpartyregistrationrepository,configured more than one asserting party>>, it first redirects to a picker page.
  18. 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`>>.
  19. image:{icondir}/number_5.png[] Then the browser takes this `<saml2:AuthnRequest>` and presents it to the asserting party.
  20. The asserting party tries to authentication the user.
  21. If successful, it returns a `<saml2:Response>` back to the browser.
  22. image:{icondir}/number_6.png[] The browser then POSTs the `<saml2:Response>` to the assertion consumer service endpoint.
  23. The following image shows how Spring Security authenticates a `<saml2:Response>`.
  24. [[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
  25. .Authenticating a `<saml2:Response>`
  26. image::{figures}/saml2webssoauthenticationfilter.png[]
  27. [NOTE]
  28. ====
  29. The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
  30. ====
  31. [[servlet-saml2login-authentication-saml2authenticationtokenconverter]]
  32. image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[delegates to `Saml2WebSsoAuthenticationFilter`].
  33. This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
  34. This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
  35. image:{icondir}/number_2.png[] Next, the filter passes the token to its configured xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`].
  36. By default, it uses the <<servlet-saml2login-architecture,`OpenSamlAuthenticationProvider`>>.
  37. image:{icondir}/number_3.png[] If authentication fails, then _Failure_.
  38. * The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] is cleared out.
  39. * The xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is invoked to restart the authentication process.
  40. image:{icondir}/number_4.png[] If authentication is successful, then _Success_.
  41. * The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`].
  42. * The `Saml2WebSsoAuthenticationFilter` invokes `FilterChain#doFilter(request,response)` to continue with the rest of the application logic.
  43. [[servlet-saml2login-minimaldependencies]]
  44. == Minimal Dependencies
  45. SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`.
  46. It builds off of the OpenSAML library.
  47. [[servlet-saml2login-minimalconfiguration]]
  48. == Minimal Configuration
  49. When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a service provider consists of two basic steps:
  50. . Include the needed dependencies.
  51. . Indicate the necessary asserting party metadata.
  52. [NOTE]
  53. Also, this configuration presupposes that you have already xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[registered the relying party with your asserting party].
  54. [[saml2-specifying-identity-provider-metadata]]
  55. === Specifying Identity Provider Metadata
  56. In a Spring Boot application, to specify an identity provider's metadata, create configuration similar to the following:
  57. ====
  58. [source,yml]
  59. ----
  60. spring:
  61. security:
  62. saml2:
  63. relyingparty:
  64. registration:
  65. adfs:
  66. identityprovider:
  67. entity-id: https://idp.example.com/issuer
  68. verification.credentials:
  69. - certificate-location: "classpath:idp.crt"
  70. singlesignon.url: https://idp.example.com/issuer/sso
  71. singlesignon.sign-request: false
  72. ----
  73. ====
  74. where:
  75. * `https://idp.example.com/issuer` is the value contained in the `Issuer` attribute of the SAML responses that the identity provider issues.
  76. * `classpath:idp.crt` is the location on the classpath for the identity provider's certificate for verifying SAML responses.
  77. * `https://idp.example.com/issuer/sso` is the endpoint where the identity provider is expecting `AuthnRequest` instances.
  78. * `adfs` is <<servlet-saml2login-relyingpartyregistrationid, an arbitrary identifier you choose>>
  79. And that's it!
  80. [NOTE]
  81. ====
  82. Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party.
  83. These are frequently abbreviated as AP and RP, respectively.
  84. ====
  85. === Runtime Expectations
  86. As configured <<saml2-specifying-identity-provider-metadata,earlier>>, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
  87. ====
  88. [source,http]
  89. ----
  90. POST /login/saml2/sso/adfs HTTP/1.1
  91. SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
  92. ----
  93. ====
  94. There are two ways to induce your asserting party to generate a `SAMLResponse`:
  95. * You can navigate to your asserting party.
  96. It likely has some kind of link or button for each registered relying party that you can click to send the `SAMLResponse`.
  97. * You can navigate to a protected page in your application -- for example, `http://localhost:8080`.
  98. Your application then redirects to the configured asserting party, which then sends the `SAMLResponse`.
  99. From here, consider jumping to:
  100. * <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
  101. * xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticatedprincipal[How to Use the `Saml2AuthenticatedPrincipal`]
  102. * <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
  103. [[servlet-saml2login-architecture]]
  104. == How SAML 2.0 Login Integrates with OpenSAML
  105. Spring Security's SAML 2.0 support has a couple of design goals:
  106. * Rely on a library for SAML 2.0 operations and domain objects.
  107. To achieve this, Spring Security uses OpenSAML.
  108. * Ensure that this library is not required when using Spring Security's SAML support.
  109. To achieve this, any interfaces or classes where Spring Security uses OpenSAML in the contract remain encapsulated.
  110. This makes it possible for you to switch out OpenSAML for some other library or an unsupported version of OpenSAML.
  111. As a natural outcome of these two goals, Spring Security's SAML API is quite small relative to other modules.
  112. Instead, such classes as `OpenSamlAuthenticationRequestFactory` and `OpenSamlAuthenticationProvider` expose `Converter` implementations that customize various steps in the authentication process.
  113. For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter delegates to `OpenSamlAuthenticationProvider`:
  114. .Authenticating an OpenSAML `Response`
  115. image:{figures}/opensamlauthenticationprovider.png[]
  116. This figure builds off of the <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter` diagram>>.
  117. image:{icondir}/number_1.png[] The `Saml2WebSsoAuthenticationFilter` formulates the `Saml2AuthenticationToken` and invokes the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`].
  118. image:{icondir}/number_2.png[] The xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`] invokes the OpenSAML authentication provider.
  119. image:{icondir}/number_3.png[] The authentication provider deserializes the response into an OpenSAML `Response` and checks its signature.
  120. If the signature is invalid, authentication fails.
  121. image:{icondir}/number_4.png[] Then the provider xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-opensamlauthenticationprovider-decryption[decrypts any `EncryptedAssertion` elements].
  122. If any decryptions fail, authentication fails.
  123. image:{icondir}/number_5.png[] Next, the provider validates the response's `Issuer` and `Destination` values.
  124. If they do not match what's in the `RelyingPartyRegistration`, authentication fails.
  125. image:{icondir}/number_6.png[] After that, the provider verifies the signature of each `Assertion`.
  126. If any signature is invalid, authentication fails.
  127. Also, if neither the response nor the assertions have signatures, authentication fails.
  128. Either the response or all the assertions must have signatures.
  129. image:{icondir}/number_7.png[] Then, the provider xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-opensamlauthenticationprovider-decryption[,]decrypts any `EncryptedID` or `EncryptedAttribute` elements].
  130. If any decryptions fail, authentication fails.
  131. image:{icondir}/number_8.png[] Next, the provider validates each assertion's `ExpiresAt` and `NotBefore` timestamps, the `<Subject>` and any `<AudienceRestriction>` conditions.
  132. If any validations fail, authentication fails.
  133. image:{icondir}/number_9.png[] Following that, the provider takes the first assertion's `AttributeStatement` and maps it to a `Map<String, List<Object>>`.
  134. It also grants the `ROLE_USER` granted authority.
  135. 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`.
  136. Then, it places that principal and the authorities into a `Saml2Authentication`.
  137. The resulting `Authentication#getPrincipal` is a Spring Security `Saml2AuthenticatedPrincipal` object, and `Authentication#getName` maps to the first assertion's `NameID` element.
  138. `Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId` holds the <<servlet-saml2login-relyingpartyregistrationid,identifier to the associated `RelyingPartyRegistration`>>.
  139. [[servlet-saml2login-opensaml-customization]]
  140. === Customizing OpenSAML Configuration
  141. Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class:
  142. ====
  143. .Java
  144. [source,java,role="primary"]
  145. ----
  146. static {
  147. OpenSamlInitializationService.initialize();
  148. }
  149. ----
  150. .Kotlin
  151. [source,kotlin,role="secondary"]
  152. ----
  153. companion object {
  154. init {
  155. OpenSamlInitializationService.initialize()
  156. }
  157. }
  158. ----
  159. ====
  160. This replaces OpenSAML's `InitializationService#initialize`.
  161. Occasionally, it can be valuable to customize how OpenSAML builds, marshalls, and unmarshalls SAML objects.
  162. In these circumstances, you may instead want to call `OpenSamlInitializationService#requireInitialize(Consumer)` that gives you access to OpenSAML's `XMLObjectProviderFactory`.
  163. For example, when sending an unsigned AuthNRequest, you may want to force reauthentication.
  164. In that case, you can register your own `AuthnRequestMarshaller`, like so:
  165. ====
  166. .Java
  167. [source,java,role="primary"]
  168. ----
  169. static {
  170. OpenSamlInitializationService.requireInitialize(factory -> {
  171. AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
  172. @Override
  173. public Element marshall(XMLObject object, Element element) throws MarshallingException {
  174. configureAuthnRequest((AuthnRequest) object);
  175. return super.marshall(object, element);
  176. }
  177. public Element marshall(XMLObject object, Document document) throws MarshallingException {
  178. configureAuthnRequest((AuthnRequest) object);
  179. return super.marshall(object, document);
  180. }
  181. private void configureAuthnRequest(AuthnRequest authnRequest) {
  182. authnRequest.setForceAuthn(true);
  183. }
  184. }
  185. factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
  186. });
  187. }
  188. ----
  189. .Kotlin
  190. [source,kotlin,role="secondary"]
  191. ----
  192. companion object {
  193. init {
  194. OpenSamlInitializationService.requireInitialize {
  195. val marshaller = object : AuthnRequestMarshaller() {
  196. override fun marshall(xmlObject: XMLObject, element: Element): Element {
  197. configureAuthnRequest(xmlObject as AuthnRequest)
  198. return super.marshall(xmlObject, element)
  199. }
  200. override fun marshall(xmlObject: XMLObject, document: Document): Element {
  201. configureAuthnRequest(xmlObject as AuthnRequest)
  202. return super.marshall(xmlObject, document)
  203. }
  204. private fun configureAuthnRequest(authnRequest: AuthnRequest) {
  205. authnRequest.isForceAuthn = true
  206. }
  207. }
  208. it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
  209. }
  210. }
  211. }
  212. ----
  213. ====
  214. The `requireInitialize` method may be called only once per application instance.
  215. [[servlet-saml2login-sansboot]]
  216. == Overriding or Replacing Boot Auto Configuration
  217. Spring Boot generates two `@Bean` objects for a relying party.
  218. The first is a `SecurityFilterChain` that configures the application as a relying party.
  219. When including `spring-security-saml2-service-provider`, the `SecurityFilterChain` looks like:
  220. .Default SAML 2.0 Login Configuration
  221. ====
  222. .Java
  223. [source,java,role="primary"]
  224. ----
  225. @Bean
  226. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  227. http
  228. .authorizeHttpRequests(authorize -> authorize
  229. .anyRequest().authenticated()
  230. )
  231. .saml2Login(withDefaults());
  232. return http.build();
  233. }
  234. ----
  235. .Kotlin
  236. [source,kotlin,role="secondary"]
  237. ----
  238. @Bean
  239. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  240. http {
  241. authorizeRequests {
  242. authorize(anyRequest, authenticated)
  243. }
  244. saml2Login { }
  245. }
  246. return http.build()
  247. }
  248. ----
  249. ====
  250. If the application does not expose a `SecurityFilterChain` bean, Spring Boot exposes the preceding default one.
  251. You can replace this by exposing the bean within the application:
  252. .Custom SAML 2.0 Login Configuration
  253. ====
  254. .Java
  255. [source,java,role="primary"]
  256. ----
  257. @Configuration
  258. @EnableWebSecurity
  259. public class MyCustomSecurityConfiguration {
  260. @Bean
  261. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  262. http
  263. .authorizeHttpRequests(authorize -> authorize
  264. .requestMatchers("/messages/**").hasAuthority("ROLE_USER")
  265. .anyRequest().authenticated()
  266. )
  267. .saml2Login(withDefaults());
  268. return http.build();
  269. }
  270. }
  271. ----
  272. .Kotlin
  273. [source,kotlin,role="secondary"]
  274. ----
  275. @Configuration
  276. @EnableWebSecurity
  277. class MyCustomSecurityConfiguration {
  278. @Bean
  279. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  280. http {
  281. authorizeRequests {
  282. authorize("/messages/**", hasAuthority("ROLE_USER"))
  283. authorize(anyRequest, authenticated)
  284. }
  285. saml2Login {
  286. }
  287. }
  288. return http.build()
  289. }
  290. }
  291. ----
  292. ====
  293. The preceding example requires the role of `USER` for any URL that starts with `/messages/`.
  294. [[servlet-saml2login-relyingpartyregistrationrepository]]
  295. 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.
  296. This includes such things as the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.
  297. You can override the default by publishing your own `RelyingPartyRegistrationRepository` bean.
  298. For example, you can look up the asserting party's configuration by hitting its metadata endpoint:
  299. .Relying Party Registration Repository
  300. ====
  301. .Java
  302. [source,java,role="primary"]
  303. ----
  304. @Value("${metadata.location}")
  305. String assertingPartyMetadataLocation;
  306. @Bean
  307. public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
  308. RelyingPartyRegistration registration = RelyingPartyRegistrations
  309. .fromMetadataLocation(assertingPartyMetadataLocation)
  310. .registrationId("example")
  311. .build();
  312. return new InMemoryRelyingPartyRegistrationRepository(registration);
  313. }
  314. ----
  315. .Kotlin
  316. [source,kotlin,role="secondary"]
  317. ----
  318. @Value("\${metadata.location}")
  319. var assertingPartyMetadataLocation: String? = null
  320. @Bean
  321. open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
  322. val registration = RelyingPartyRegistrations
  323. .fromMetadataLocation(assertingPartyMetadataLocation)
  324. .registrationId("example")
  325. .build()
  326. return InMemoryRelyingPartyRegistrationRepository(registration)
  327. }
  328. ----
  329. ====
  330. [[servlet-saml2login-relyingpartyregistrationid]]
  331. [NOTE]
  332. The `registrationId` is an arbitrary value that you choose for differentiating between registrations.
  333. Alternatively, you can provide each detail manually:
  334. .Relying Party Registration Repository Manual Configuration
  335. ====
  336. .Java
  337. [source,java,role="primary"]
  338. ----
  339. @Value("${verification.key}")
  340. File verificationKey;
  341. @Bean
  342. public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
  343. X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
  344. Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
  345. RelyingPartyRegistration registration = RelyingPartyRegistration
  346. .withRegistrationId("example")
  347. .assertingPartyDetails(party -> party
  348. .entityId("https://idp.example.com/issuer")
  349. .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
  350. .wantAuthnRequestsSigned(false)
  351. .verificationX509Credentials(c -> c.add(credential))
  352. )
  353. .build();
  354. return new InMemoryRelyingPartyRegistrationRepository(registration);
  355. }
  356. ----
  357. .Kotlin
  358. [source,kotlin,role="secondary"]
  359. ----
  360. @Value("\${verification.key}")
  361. var verificationKey: File? = null
  362. @Bean
  363. open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
  364. val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
  365. val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
  366. val registration = RelyingPartyRegistration
  367. .withRegistrationId("example")
  368. .assertingPartyDetails { party: AssertingPartyDetails.Builder ->
  369. party
  370. .entityId("https://idp.example.com/issuer")
  371. .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
  372. .wantAuthnRequestsSigned(false)
  373. .verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
  374. c.add(
  375. credential
  376. )
  377. }
  378. }
  379. .build()
  380. return InMemoryRelyingPartyRegistrationRepository(registration)
  381. }
  382. ----
  383. ====
  384. [NOTE]
  385. ====
  386. `X509Support` is an OpenSAML class, used in the preceding snippet for brevity.
  387. ====
  388. [[servlet-saml2login-relyingpartyregistrationrepository-dsl]]
  389. Alternatively, you can directly wire up the repository by using the DSL, which also overrides the auto-configured `SecurityFilterChain`:
  390. .Custom Relying Party Registration DSL
  391. ====
  392. .Java
  393. [source,java,role="primary"]
  394. ----
  395. @Configuration
  396. @EnableWebSecurity
  397. public class MyCustomSecurityConfiguration {
  398. @Bean
  399. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  400. http
  401. .authorizeHttpRequests(authorize -> authorize
  402. .requestMatchers("/messages/**").hasAuthority("ROLE_USER")
  403. .anyRequest().authenticated()
  404. )
  405. .saml2Login(saml2 -> saml2
  406. .relyingPartyRegistrationRepository(relyingPartyRegistrations())
  407. );
  408. return http.build();
  409. }
  410. }
  411. ----
  412. .Kotlin
  413. [source,kotlin,role="secondary"]
  414. ----
  415. @Configuration
  416. @EnableWebSecurity
  417. class MyCustomSecurityConfiguration {
  418. @Bean
  419. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  420. http {
  421. authorizeRequests {
  422. authorize("/messages/**", hasAuthority("ROLE_USER"))
  423. authorize(anyRequest, authenticated)
  424. }
  425. saml2Login {
  426. relyingPartyRegistrationRepository = relyingPartyRegistrations()
  427. }
  428. }
  429. return http.build()
  430. }
  431. }
  432. ----
  433. ====
  434. [NOTE]
  435. ====
  436. A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`.
  437. ====
  438. [[servlet-saml2login-relyingpartyregistration]]
  439. == RelyingPartyRegistration
  440. A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
  441. instance represents a link between an relying party and an asserting party's metadata.
  442. 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.
  443. 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.
  444. The following `RelyingPartyRegistration` is the minimum required for most setups:
  445. ====
  446. .Java
  447. [source,java,role="primary"]
  448. ----
  449. RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
  450. .fromMetadataLocation("https://ap.example.org/metadata")
  451. .registrationId("my-id")
  452. .build();
  453. ----
  454. .Kotlin
  455. [source,kotlin,role="secondary"]
  456. ----
  457. val relyingPartyRegistration = RelyingPartyRegistrations
  458. .fromMetadataLocation("https://ap.example.org/metadata")
  459. .registrationId("my-id")
  460. .build()
  461. ----
  462. ====
  463. Note that you can also create a `RelyingPartyRegistration` from an arbitrary `InputStream` source.
  464. One such example is when the metadata is stored in a database:
  465. [source,java]
  466. ----
  467. String xml = fromDatabase();
  468. try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
  469. RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
  470. .fromMetadata(source)
  471. .registrationId("my-id")
  472. .build();
  473. }
  474. ----
  475. A more sophisticated setup is also possible:
  476. ====
  477. .Java
  478. [source,java,role="primary"]
  479. ----
  480. RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
  481. .entityId("{baseUrl}/{registrationId}")
  482. .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
  483. .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
  484. .assertingPartyDetails(party -> party
  485. .entityId("https://ap.example.org")
  486. .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
  487. .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
  488. )
  489. .build();
  490. ----
  491. .Kotlin
  492. [source,kotlin,role="secondary"]
  493. ----
  494. val relyingPartyRegistration =
  495. RelyingPartyRegistration.withRegistrationId("my-id")
  496. .entityId("{baseUrl}/{registrationId}")
  497. .decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
  498. c.add(relyingPartyDecryptingCredential())
  499. }
  500. .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
  501. .assertingPartyDetails { party -> party
  502. .entityId("https://ap.example.org")
  503. .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
  504. .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
  505. }
  506. .build()
  507. ----
  508. ====
  509. [TIP]
  510. ====
  511. The top-level metadata methods are details about the relying party.
  512. The methods inside `assertingPartyDetails` are details about the asserting party.
  513. ====
  514. [NOTE]
  515. ====
  516. The location where a relying party is expecting SAML Responses is the Assertion Consumer Service Location.
  517. ====
  518. The default for the relying party's `entityId` is `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`.
  519. This is this value needed when configuring the asserting party to know about your relying party.
  520. The default for the `assertionConsumerServiceLocation` is `+/login/saml2/sso/{registrationId}+`.
  521. By default, it is mapped to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
  522. [[servlet-saml2login-rpr-uripatterns]]
  523. === URI Patterns
  524. You probably noticed the `+{baseUrl}+` and `+{registrationId}+` placeholders in the preceding examples.
  525. These are useful for generating URIs. As a result, the relying party's `entityId` and `assertionConsumerServiceLocation` support the following placeholders:
  526. * `baseUrl` - the scheme, host, and port of a deployed application
  527. * `registrationId` - the registration id for this relying party
  528. * `baseScheme` - the scheme of a deployed application
  529. * `baseHost` - the host of a deployed application
  530. * `basePort` - the port of a deployed application
  531. For example, the `assertionConsumerServiceLocation` defined earlier was:
  532. `+/my-login-endpoint/{registrationId}+`
  533. In a deployed application, it translates to:
  534. `+/my-login-endpoint/adfs+`
  535. The `entityId` shown earlier was defined as:
  536. `+{baseUrl}/{registrationId}+`
  537. In a deployed application, that translates to:
  538. `+https://rp.example.com/adfs+`
  539. The prevailing URI patterns are as follows:
  540. * `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a `<saml2:AuthnRequest>`] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party
  541. * `+/saml2/login/sso/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's `<saml2:Response>`] based on the configurations for that `RelyingPartyRegistration`
  542. * `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `<saml2:LogoutRequest>` and `<saml2:LogoutResponse>` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state
  543. * `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration`
  544. Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios.
  545. If you wish to remove the `registrationId` from the URL for any reason, you can <<servlet-saml2login-rpr-relyingpartyregistrationresolver,specify a `RelyingPartyRegistrationResolver`>> to tell Spring Security how to look up the `registrationId`.
  546. [[servlet-saml2login-rpr-credentials]]
  547. === Credentials
  548. In the example shown <<servlet-saml2login-relyingpartyregistration,earlier>>, you also likely noticed the credential that was used.
  549. Oftentimes, a relying party uses the same key to sign payloads as well as decrypt them.
  550. Alternatively, it can use the same key to verify payloads as well as encrypt them.
  551. Because of this, Spring Security ships with `Saml2X509Credential`, a SAML-specific credential that simplifies configuring the same key for different use cases.
  552. At a minimum, you need to have a certificate from the asserting party so that the asserting party's signed responses can be verified.
  553. To construct a `Saml2X509Credential` that you can use to verify assertions from the asserting party, you can load the file and use
  554. the `CertificateFactory`:
  555. ====
  556. .Java
  557. [source,java,role="primary"]
  558. ----
  559. Resource resource = new ClassPathResource("ap.crt");
  560. try (InputStream is = resource.getInputStream()) {
  561. X509Certificate certificate = (X509Certificate)
  562. CertificateFactory.getInstance("X.509").generateCertificate(is);
  563. return Saml2X509Credential.verification(certificate);
  564. }
  565. ----
  566. .Kotlin
  567. [source,kotlin,role="secondary"]
  568. ----
  569. val resource = ClassPathResource("ap.crt")
  570. resource.inputStream.use {
  571. return Saml2X509Credential.verification(
  572. CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
  573. )
  574. }
  575. ----
  576. ====
  577. Suppose that the asserting party is going to also encrypt the assertion.
  578. In that case, the relying party needs a private key to decrypt the encrypted value.
  579. In that case, you need an `RSAPrivateKey` as well as its corresponding `X509Certificate`.
  580. You can load the first by using Spring Security's `RsaKeyConverters` utility class and the second as you did before:
  581. ====
  582. .Java
  583. [source,java,role="primary"]
  584. ----
  585. X509Certificate certificate = relyingPartyDecryptionCertificate();
  586. Resource resource = new ClassPathResource("rp.crt");
  587. try (InputStream is = resource.getInputStream()) {
  588. RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
  589. return Saml2X509Credential.decryption(rsa, certificate);
  590. }
  591. ----
  592. .Kotlin
  593. [source,kotlin,role="secondary"]
  594. ----
  595. val certificate: X509Certificate = relyingPartyDecryptionCertificate()
  596. val resource = ClassPathResource("rp.crt")
  597. resource.inputStream.use {
  598. val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
  599. return Saml2X509Credential.decryption(rsa, certificate)
  600. }
  601. ----
  602. ====
  603. [TIP]
  604. ====
  605. When you specify the locations of these files as the appropriate Spring Boot properties, Spring Boot performs these conversions for you.
  606. ====
  607. [[servlet-saml2login-rpr-duplicated]]
  608. === Duplicated Relying Party Configurations
  609. When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances:
  610. * The relying party's `entityId`
  611. * Its `assertionConsumerServiceLocation`
  612. * Its credentials -- for example, its signing or decryption credentials
  613. This setup may let credentials be more easily rotated for some identity providers versus others.
  614. The duplication can be alleviated in a few different ways.
  615. First, in YAML this can be alleviated with references:
  616. ====
  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. ====
  635. Second, in a database, you need not replicate the model of `RelyingPartyRegistration`.
  636. Third, in Java, you can create a custom configuration method:
  637. ====
  638. .Java
  639. [source,java,role="primary"]
  640. ----
  641. private RelyingPartyRegistration.Builder
  642. addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {
  643. Saml2X509Credential signingCredential = ...
  644. builder.signingX509Credentials(c -> c.addAll(signingCredential));
  645. // ... other relying party configurations
  646. }
  647. @Bean
  648. public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
  649. RelyingPartyRegistration okta = addRelyingPartyDetails(
  650. RelyingPartyRegistrations
  651. .fromMetadataLocation(oktaMetadataUrl)
  652. .registrationId("okta")).build();
  653. RelyingPartyRegistration azure = addRelyingPartyDetails(
  654. RelyingPartyRegistrations
  655. .fromMetadataLocation(oktaMetadataUrl)
  656. .registrationId("azure")).build();
  657. return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
  658. }
  659. ----
  660. .Kotlin
  661. [source,kotlin,role="secondary"]
  662. ----
  663. private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
  664. val signingCredential: Saml2X509Credential = ...
  665. builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
  666. c.add(
  667. signingCredential
  668. )
  669. }
  670. // ... other relying party configurations
  671. }
  672. @Bean
  673. open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
  674. val okta = addRelyingPartyDetails(
  675. RelyingPartyRegistrations
  676. .fromMetadataLocation(oktaMetadataUrl)
  677. .registrationId("okta")
  678. ).build()
  679. val azure = addRelyingPartyDetails(
  680. RelyingPartyRegistrations
  681. .fromMetadataLocation(oktaMetadataUrl)
  682. .registrationId("azure")
  683. ).build()
  684. return InMemoryRelyingPartyRegistrationRepository(okta, azure)
  685. }
  686. ----
  687. ====
  688. [[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
  689. === Resolving the `RelyingPartyRegistration` from the Request
  690. As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
  691. There are a number of reasons you may want to customize that. Among them:
  692. * You may already <<relyingpartyregistrationresolver-single, know which `RelyingPartyRegistration` you need>>
  693. * You may be <<relyingpartyregistrationresolver-entityid, federating many asserting parties>>
  694. To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`.
  695. The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
  696. [NOTE]
  697. Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
  698. [[relyingpartyregistrationresolver-single]]
  699. ==== Resolving to a Single Consistent `RelyingPartyRegistration`
  700. You can provide a resolver that, for example, always returns the same `RelyingPartyRegistration`:
  701. ====
  702. .Java
  703. [source,java,role="primary"]
  704. ----
  705. public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
  706. private final RelyingPartyRegistrationResolver delegate;
  707. public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
  708. this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
  709. }
  710. @Override
  711. public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
  712. return this.delegate.resolve(request, "single");
  713. }
  714. }
  715. ----
  716. .Kotlin
  717. [source,kotlin,role="secondary"]
  718. ----
  719. class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
  720. override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
  721. return this.delegate.resolve(request, "single")
  722. }
  723. }
  724. ----
  725. ====
  726. [TIP]
  727. You might next take a look at how to use this resolver to customize xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[`<saml2:SPSSODescriptor>` metadata production].
  728. [[relyingpartyregistrationresolver-entityid]]
  729. ==== Resolving Based on the `<saml2:Response#Issuer>`
  730. When you have one relying party that can accept assertions from multiple asserting parties, you will have as many ``RelyingPartyRegistration``s as asserting parties, with the <<servlet-saml2login-rpr-duplicated, relying party information duplicated across each instance>>.
  731. This carries the implication that the assertion consumer service endpoint will be different for each asserting party, which may not be desirable.
  732. You can instead resolve the `registrationId` via the `Issuer`.
  733. A custom implementation of `RelyingPartyRegistrationResolver` that does this may look like:
  734. ====
  735. .Java
  736. [source,java,role="primary"]
  737. ----
  738. public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
  739. private final InMemoryRelyingPartyRegistrationRepository registrations;
  740. // ... constructor
  741. @Override
  742. RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
  743. if (registrationId != null) {
  744. return this.registrations.findByRegistrationId(registrationId);
  745. }
  746. String entityId = resolveEntityIdFromSamlResponse(request);
  747. for (RelyingPartyRegistration registration : this.registrations) {
  748. if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
  749. return registration;
  750. }
  751. }
  752. return null;
  753. }
  754. private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
  755. // ...
  756. }
  757. }
  758. ----
  759. .Kotlin
  760. [source,kotlin,role="secondary"]
  761. ----
  762. class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
  763. RelyingPartyRegistrationResolver {
  764. @Override
  765. fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
  766. if (registrationId != null) {
  767. return this.registrations.findByRegistrationId(registrationId)
  768. }
  769. String entityId = resolveEntityIdFromSamlResponse(request)
  770. for (val registration : this.registrations) {
  771. if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
  772. return registration
  773. }
  774. }
  775. return null
  776. }
  777. private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
  778. // ...
  779. }
  780. }
  781. ----
  782. ====
  783. [TIP]
  784. You might next take a look at how to use this resolver to customize xref:servlet/saml2/login/authentication.adoc#relyingpartyregistrationresolver-apply[`<saml2:Response>` authentication].
  785. [[federating-saml2-login]]
  786. === Federating Login
  787. One common arrangement with SAML 2.0 is an identity provider that has multiple asserting parties.
  788. In this case, the identity provider's metadata endpoint returns multiple `<md:IDPSSODescriptor>` elements.
  789. These multiple asserting parties can be accessed in a single call to `RelyingPartyRegistrations` like so:
  790. ====
  791. .Java
  792. [source,java,role="primary"]
  793. ----
  794. Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
  795. .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
  796. .stream().map((builder) -> builder
  797. .registrationId(UUID.randomUUID().toString())
  798. .entityId("https://example.org/saml2/sp")
  799. .build()
  800. )
  801. .collect(Collectors.toList()));
  802. ----
  803. .Kotlin
  804. [source,java,role="secondary"]
  805. ----
  806. var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
  807. .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
  808. .stream().map { builder : RelyingPartyRegistration.Builder -> builder
  809. .registrationId(UUID.randomUUID().toString())
  810. .entityId("https://example.org/saml2/sp")
  811. .build()
  812. }
  813. .collect(Collectors.toList()));
  814. ----
  815. ====
  816. Note that because the registration id is set to a random value, this will change certain SAML 2.0 endpoints to be unpredictable.
  817. There are several ways to address this; let's focus on a way that suits the specific use case of federation.
  818. In many federation cases, all the asserting parties share service provider configuration.
  819. Given that Spring Security will by default include the `registrationId` in all many of its SAML 2.0 URIs, the next step is often to change these URIs to exclude the `registrationId`.
  820. There are two main URIs you will want to change along those lines:
  821. * <<relyingpartyregistrationresolver-entityid,Resolve by `<saml2:Response#Issuer>`>>
  822. * <<relyingpartyregistrationresolver-single,Resolve with a default `RelyingPartyRegistration`>>
  823. [NOTE]
  824. Optionally, you may also want to change the Authentication Request location, but since this is a URI internal to the app and not published to asserting parties, the benefit is often minimal.
  825. You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].
  826. [[using-spring-security-saml-extension-uris]]
  827. === Using Spring Security SAML Extension URIs
  828. In the event that you are migrating from the Spring Security SAML Extension, there may be some benefit to configuring your application to use the SAML Extension URI defaults.
  829. For more information on this, please see {gh-samples-url}/servlet/spring-boot/java/saml2/custom-urls[our `custom-urls` sample] and {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].