authentication.adoc 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. [[servlet-saml2login-authenticate-responses]]
  2. = Authenticating ``<saml2:Response>``s
  3. To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-authentication-saml2authenticationtokenconverter[`Saml2AuthenticationTokenConverter`] to populate the `Authentication` request and xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] to authenticate it.
  4. You can configure this in a number of ways including:
  5. 1. Changing the way the `RelyingPartyRegistration` is Looked Up
  6. 2. Setting a clock skew to timestamp validation
  7. 3. Mapping the response to a list of `GrantedAuthority` instances
  8. 4. Customizing the strategy for validating assertions
  9. 5. Customizing the strategy for decrypting response and assertion elements
  10. To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
  11. [[saml2-response-processing-endpoint]]
  12. == Changing the SAML Response Processing Endpoint
  13. The default endpoint is `+/login/saml2/sso/{registrationId}+`.
  14. You can change this in the DSL and in the associated metadata like so:
  15. [tabs]
  16. ======
  17. Java::
  18. +
  19. [source,java,role="primary"]
  20. ----
  21. @Bean
  22. SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
  23. http
  24. // ...
  25. .saml2Login((saml2) -> saml2.loginProcessingUrl("/saml2/login/sso"))
  26. // ...
  27. return http.build();
  28. }
  29. ----
  30. Kotlin::
  31. +
  32. [source,kotlin,role="secondary"]
  33. ----
  34. @Bean
  35. fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
  36. http {
  37. // ...
  38. .saml2Login {
  39. loginProcessingUrl = "/saml2/login/sso"
  40. }
  41. // ...
  42. }
  43. return http.build()
  44. }
  45. ----
  46. ======
  47. and:
  48. [tabs]
  49. ======
  50. Java::
  51. +
  52. [source,java,role="primary"]
  53. ----
  54. relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
  55. ----
  56. Kotlin::
  57. +
  58. [source,kotlin,role="secondary"]
  59. ----
  60. relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
  61. ----
  62. ======
  63. [[relyingpartyregistrationresolver-apply]]
  64. == Changing `RelyingPartyRegistration` lookup
  65. By default, this converter will match against any associated `<saml2:AuthnRequest>` or any `registrationId` it finds in the URL.
  66. Or, if it cannot find one in either of those cases, then it attempts to look it up by the `<saml2:Response#Issuer>` element.
  67. There are a number of circumstances where you might need something more sophisticated, like if you are supporting `ARTIFACT` binding.
  68. In those cases, you can customize lookup through a custom `AuthenticationConverter`, which you can customize like so:
  69. [tabs]
  70. ======
  71. Java::
  72. +
  73. [source,java,role="primary"]
  74. ----
  75. @Bean
  76. SecurityFilterChain securityFilters(HttpSecurity http, AuthenticationConverter authenticationConverter) throws Exception {
  77. http
  78. // ...
  79. .saml2Login((saml2) -> saml2.authenticationConverter(authenticationConverter))
  80. // ...
  81. return http.build();
  82. }
  83. ----
  84. Kotlin::
  85. +
  86. [source,kotlin,role="secondary"]
  87. ----
  88. @Bean
  89. fun securityFilters(val http: HttpSecurity, val converter: AuthenticationConverter): SecurityFilterChain {
  90. http {
  91. // ...
  92. .saml2Login {
  93. authenticationConverter = converter
  94. }
  95. // ...
  96. }
  97. return http.build()
  98. }
  99. ----
  100. ======
  101. [[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
  102. == Setting a Clock Skew
  103. It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized.
  104. For that reason, you can configure ``OpenSaml4AuthenticationProvider``'s default assertion validator with some tolerance:
  105. [tabs]
  106. ======
  107. Java::
  108. +
  109. [source,java,role="primary"]
  110. ----
  111. @Configuration
  112. @EnableWebSecurity
  113. public class SecurityConfig {
  114. @Bean
  115. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  116. OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
  117. authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
  118. .createDefaultAssertionValidatorWithParameters(assertionToken -> {
  119. Map<String, Object> params = new HashMap<>();
  120. params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
  121. // ... other validation parameters
  122. return new ValidationContext(params);
  123. })
  124. );
  125. http
  126. .authorizeHttpRequests(authz -> authz
  127. .anyRequest().authenticated()
  128. )
  129. .saml2Login(saml2 -> saml2
  130. .authenticationManager(new ProviderManager(authenticationProvider))
  131. );
  132. return http.build();
  133. }
  134. }
  135. ----
  136. Kotlin::
  137. +
  138. [source,kotlin,role="secondary"]
  139. ----
  140. @Configuration
  141. @EnableWebSecurity
  142. open class SecurityConfig {
  143. @Bean
  144. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  145. val authenticationProvider = OpenSaml4AuthenticationProvider()
  146. authenticationProvider.setAssertionValidator(
  147. OpenSaml4AuthenticationProvider
  148. .createDefaultAssertionValidatorWithParameters(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
  149. val params: MutableMap<String, Any> = HashMap()
  150. params[CLOCK_SKEW] =
  151. Duration.ofMinutes(10).toMillis()
  152. ValidationContext(params)
  153. })
  154. )
  155. http {
  156. authorizeRequests {
  157. authorize(anyRequest, authenticated)
  158. }
  159. saml2Login {
  160. authenticationManager = ProviderManager(authenticationProvider)
  161. }
  162. }
  163. return http.build()
  164. }
  165. }
  166. ----
  167. ======
  168. If you are using xref:servlet/saml2/opensaml.adoc[OpenSAML 5], then we have a simpler way, using `OpenSaml5AuthenticationProvider.AssertionValidator`:
  169. [tabs]
  170. ======
  171. Java::
  172. +
  173. [source,java,role="primary"]
  174. ----
  175. @Configuration
  176. @EnableWebSecurity
  177. public class SecurityConfig {
  178. @Bean
  179. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  180. OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
  181. AssertionValidator assertionValidator = AssertionValidator.builder()
  182. .clockSkew(Duration.ofMinutes(10)).build();
  183. authenticationProvider.setAssertionValidator(assertionValidator);
  184. http
  185. .authorizeHttpRequests(authz -> authz
  186. .anyRequest().authenticated()
  187. )
  188. .saml2Login(saml2 -> saml2
  189. .authenticationManager(new ProviderManager(authenticationProvider))
  190. );
  191. return http.build();
  192. }
  193. }
  194. ----
  195. Kotlin::
  196. +
  197. [source,kotlin,role="secondary"]
  198. ----
  199. @Configuration @EnableWebSecurity
  200. class SecurityConfig {
  201. @Bean
  202. @Throws(Exception::class)
  203. fun filterChain(http: HttpSecurity): SecurityFilterChain {
  204. val authenticationProvider = OpenSaml5AuthenticationProvider()
  205. val assertionValidator = AssertionValidator.builder().clockSkew(Duration.ofMinutes(10)).build()
  206. authenticationProvider.setAssertionValidator(assertionValidator)
  207. http {
  208. authorizeHttpRequests {
  209. authorize(anyRequest, authenticated)
  210. }
  211. saml2Login {
  212. authenticationManager = ProviderManager(authenticationProvider)
  213. }
  214. }
  215. return http.build()
  216. }
  217. }
  218. ----
  219. ======
  220. == Converting an `Assertion` into an `Authentication`
  221. `OpenSamlXAuthenticationProvider#setResponseAuthenticationConverter` provides a way for you to change how it converts your assertion into an `Authentication` instance.
  222. You can set a custom converter in the following way:
  223. [tabs]
  224. ======
  225. Java::
  226. +
  227. [source,java,role="primary"]
  228. ----
  229. @Configuration
  230. @EnableWebSecurity
  231. public class SecurityConfig {
  232. @Autowired
  233. Converter<ResponseToken, Saml2Authentication> authenticationConverter;
  234. @Bean
  235. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  236. OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
  237. authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter);
  238. http
  239. .authorizeHttpRequests((authz) -> authz
  240. .anyRequest().authenticated())
  241. .saml2Login((saml2) -> saml2
  242. .authenticationManager(new ProviderManager(authenticationProvider))
  243. );
  244. return http.build();
  245. }
  246. }
  247. ----
  248. Kotlin::
  249. +
  250. [source,kotlin,role="secondary"]
  251. ----
  252. @Configuration
  253. @EnableWebSecurity
  254. open class SecurityConfig {
  255. @Autowired
  256. var authenticationConverter: Converter<ResponseToken, Saml2Authentication>? = null
  257. @Bean
  258. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  259. val authenticationProvider = OpenSaml5AuthenticationProvider()
  260. authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter)
  261. http {
  262. authorizeRequests {
  263. authorize(anyRequest, authenticated)
  264. }
  265. saml2Login {
  266. authenticationManager = ProviderManager(authenticationProvider)
  267. }
  268. }
  269. return http.build()
  270. }
  271. }
  272. ----
  273. ======
  274. The ensuing examples all build off of this common construct to show you different ways this converter comes in handy.
  275. [[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
  276. == Coordinating with a `UserDetailsService`
  277. Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
  278. In that case, the response authentication converter can come in handy, as can be seen below:
  279. [tabs]
  280. ======
  281. Java::
  282. +
  283. [source,java,role="primary"]
  284. ----
  285. @Component
  286. class MyUserDetailsResponseAuthenticationConverter implements Converter<ResponseToken, Saml2Authentication> {
  287. private final ResponseAuthenticationConverter delegate = new ResponseAuthenticationConverter();
  288. private final UserDetailsService userDetailsService;
  289. MyUserDetailsResponseAuthenticationConverter(UserDetailsService userDetailsService) {
  290. this.userDetailsService = userDetailsService;
  291. }
  292. @Override
  293. public Saml2Authentication convert(ResponseToken responseToken) {
  294. Saml2Authentication authentication = this.delegate.convert(responseToken); <1>
  295. UserDetails principal = this.userDetailsService.loadByUsername(username); <2>
  296. String saml2Response = authentication.getSaml2Response();
  297. Saml2ResponseAssertionAccessor assertion = new OpenSamlResponseAssertionAccessor(
  298. saml2Response, CollectionUtils.getFirst(response.getAssertions()));
  299. Collection<GrantedAuthority> authorities = principal.getAuthorities();
  300. return new Saml2AssertionAuthentication(userDetails, assertion, authorities); <3>
  301. }
  302. }
  303. ----
  304. Kotlin::
  305. +
  306. [source,kotlin,role="secondary"]
  307. ----
  308. @Component
  309. open class MyUserDetailsResponseAuthenticationConverter(val delegate: ResponseAuthenticationConverter,
  310. UserDetailsService userDetailsService): Converter<ResponseToken, Saml2Authentication> {
  311. @Override
  312. open fun convert(responseToken: ResponseToken): Saml2Authentication {
  313. val authentication = this.delegate.convert(responseToken) <1>
  314. val principal = this.userDetailsService.loadByUsername(username) <2>
  315. val saml2Response = authentication.getSaml2Response()
  316. val assertion = OpenSamlResponseAssertionAccessor(
  317. saml2Response, CollectionUtils.getFirst(response.getAssertions()))
  318. val authorities = principal.getAuthorities()
  319. return Saml2AssertionAuthentication(userDetails, assertion, authorities) <3>
  320. }
  321. }
  322. ----
  323. ======
  324. <1> First, call the default converter, which extracts attributes and authorities from the response
  325. <2> Second, call the xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`] using the relevant information
  326. <3> Third, return an authentication that includes the user details
  327. [TIP]
  328. ====
  329. If your `UserDetailsService` returns a value that also implements `AuthenticatedPrincipal`, then you don't need a custom authentication implementation.
  330. ====
  331. Or, if you are using OpenSaml 4, then you can achieve something similar as follows:
  332. [tabs]
  333. ======
  334. Java::
  335. +
  336. [source,java,role="primary"]
  337. ----
  338. @Configuration
  339. @EnableWebSecurity
  340. public class SecurityConfig {
  341. @Autowired
  342. UserDetailsService userDetailsService;
  343. @Bean
  344. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  345. OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
  346. authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
  347. Saml2Authentication authentication = OpenSaml4AuthenticationProvider
  348. .createDefaultResponseAuthenticationConverter() <1>
  349. .convert(responseToken);
  350. Assertion assertion = responseToken.getResponse().getAssertions().get(0);
  351. String username = assertion.getSubject().getNameID().getValue();
  352. UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); <2>
  353. return MySaml2Authentication(userDetails, authentication); <3>
  354. });
  355. http
  356. .authorizeHttpRequests(authz -> authz
  357. .anyRequest().authenticated()
  358. )
  359. .saml2Login(saml2 -> saml2
  360. .authenticationManager(new ProviderManager(authenticationProvider))
  361. );
  362. return http.build();
  363. }
  364. }
  365. ----
  366. Kotlin::
  367. +
  368. [source,kotlin,role="secondary"]
  369. ----
  370. @Configuration
  371. @EnableWebSecurity
  372. open class SecurityConfig {
  373. @Autowired
  374. var userDetailsService: UserDetailsService? = null
  375. @Bean
  376. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  377. val authenticationProvider = OpenSaml4AuthenticationProvider()
  378. authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
  379. val authentication = OpenSaml4AuthenticationProvider
  380. .createDefaultResponseAuthenticationConverter() <1>
  381. .convert(responseToken)
  382. val assertion: Assertion = responseToken.response.assertions[0]
  383. val username: String = assertion.subject.nameID.value
  384. val userDetails = userDetailsService!!.loadUserByUsername(username) <2>
  385. MySaml2Authentication(userDetails, authentication) <3>
  386. }
  387. http {
  388. authorizeRequests {
  389. authorize(anyRequest, authenticated)
  390. }
  391. saml2Login {
  392. authenticationManager = ProviderManager(authenticationProvider)
  393. }
  394. }
  395. return http.build()
  396. }
  397. }
  398. ----
  399. ======
  400. <1> First, call the default converter, which extracts attributes and authorities from the response
  401. <2> Second, call the xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`] using the relevant information
  402. <3> Third, return a custom authentication that includes the user details
  403. [NOTE]
  404. It's not required to call ``OpenSaml4AuthenticationProvider``'s default authentication converter.
  405. It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from ``AttributeStatement``s as well as the single `ROLE_USER` authority.
  406. === Configuring the Principal Name
  407. Sometimes, the principal name is not in the `<saml2:NameID>` element.
  408. In that case, you can configure the `ResponseAuthenticationConverter` with a custom strategy like so:
  409. [tabs]
  410. ======
  411. Java::
  412. +
  413. [source,java,role="primary"]
  414. ----
  415. @Bean
  416. ResponseAuthenticationConverter authenticationConverter() {
  417. ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
  418. authenticationConverter.setPrincipalNameConverter((assertion) -> {
  419. // ... work with OpenSAML's Assertion object to extract the principal
  420. });
  421. return authenticationConverter;
  422. }
  423. ----
  424. Kotlin::
  425. +
  426. [source,kotlin,role="secondary"]
  427. ----
  428. @Bean
  429. fun authenticationConverter(): ResponseAuthenticationConverter {
  430. val authenticationConverter: ResponseAuthenticationConverter = ResponseAuthenticationConverter()
  431. authenticationConverter.setPrincipalNameConverter { assertion ->
  432. // ... work with OpenSAML's Assertion object to extract the principal
  433. }
  434. return authenticationConverter
  435. }
  436. ----
  437. ======
  438. === Configuring a Principal's Granted Authorities
  439. Spring Security automatically grants `ROLE_USER` when using `OpenSamlXAuhenticationProvider`.
  440. With `OpenSaml5AuthenticationProvider`, you can configure a different set of granted authorities like so:
  441. [tabs]
  442. ======
  443. Java::
  444. +
  445. [source,java,role="primary"]
  446. ----
  447. @Bean
  448. ResponseAuthenticationConverter authenticationConverter() {
  449. ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
  450. authenticationConverter.setPrincipalNameConverter((assertion) -> {
  451. // ... grant the needed authorities based on attributes in the assertion
  452. });
  453. return authenticationConverter;
  454. }
  455. ----
  456. Kotlin::
  457. +
  458. [source,kotlin,role="secondary"]
  459. ----
  460. @Bean
  461. fun authenticationConverter(): ResponseAuthenticationConverter {
  462. val authenticationConverter = ResponseAuthenticationConverter()
  463. authenticationConverter.setPrincipalNameConverter{ assertion ->
  464. // ... grant the needed authorities based on attributes in the assertion
  465. }
  466. return authenticationConverter
  467. }
  468. ----
  469. ======
  470. [[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
  471. == Performing Additional Response Validation
  472. `OpenSaml4AuthenticationProvider` validates the `Issuer` and `Destination` values right after decrypting the `Response`.
  473. You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.
  474. For example, you can throw a custom exception with any additional information available in the `Response` object, like so:
  475. [source,java]
  476. ----
  477. OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
  478. provider.setResponseValidator((responseToken) -> {
  479. Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
  480. .createDefaultResponseValidator()
  481. .convert(responseToken)
  482. .concat(myCustomValidator.convert(responseToken));
  483. if (!result.getErrors().isEmpty()) {
  484. String inResponseTo = responseToken.getInResponseTo();
  485. throw new CustomSaml2AuthenticationException(result, inResponseTo);
  486. }
  487. return result;
  488. });
  489. ----
  490. When using `OpenSaml5AuthenticationProvider`, you can do the same with less boilerplate:
  491. [source,java]
  492. ----
  493. OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
  494. ResponseValidator responseValidator = ResponseValidator.withDefaults(myCustomValidator);
  495. provider.setResponseValidator(responseValidator);
  496. ----
  497. You can also customize which validation steps Spring Security should do.
  498. For example, if you want to skip `Response#InResponseTo` validation, you can call ``ResponseValidator``'s constructor, excluding `InResponseToValidator` from the list:
  499. [source,java]
  500. ----
  501. OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
  502. ResponseValidator responseValidator = new ResponseValidator(new DestinationValidator(), new IssuerValidator());
  503. provider.setResponseValidator(responseValidator);
  504. ----
  505. [TIP]
  506. ====
  507. OpenSAML performs `Asssertion#InResponseTo` validation in its `BearerSubjectConfirmationValidator` class, which is configurable using <<_performing_additional_assertion_validation, setAssertionValidator>>.
  508. ====
  509. == Performing Additional Assertion Validation
  510. `OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
  511. After verifying the signature, it will:
  512. 1. Validate `<AudienceRestriction>` and `<DelegationRestriction>` conditions
  513. 2. Validate ``<SubjectConfirmation>``s, expect for any IP address information
  514. To perform additional validation, you can configure your own assertion validator that delegates to ``OpenSaml4AuthenticationProvider``'s default and then performs its own.
  515. [[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]]
  516. For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
  517. [tabs]
  518. ======
  519. Java::
  520. +
  521. [source,java,role="primary"]
  522. ----
  523. OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
  524. OneTimeUseConditionValidator validator = ...;
  525. provider.setAssertionValidator(assertionToken -> {
  526. Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
  527. .createDefaultAssertionValidator()
  528. .convert(assertionToken);
  529. Assertion assertion = assertionToken.getAssertion();
  530. OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
  531. ValidationContext context = new ValidationContext();
  532. try {
  533. if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
  534. return result;
  535. }
  536. } catch (Exception e) {
  537. return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
  538. }
  539. return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
  540. });
  541. ----
  542. Kotlin::
  543. +
  544. [source,kotlin,role="secondary"]
  545. ----
  546. var provider = OpenSaml4AuthenticationProvider()
  547. var validator: OneTimeUseConditionValidator = ...
  548. provider.setAssertionValidator { assertionToken ->
  549. val result = OpenSaml4AuthenticationProvider
  550. .createDefaultAssertionValidator()
  551. .convert(assertionToken)
  552. val assertion: Assertion = assertionToken.assertion
  553. val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
  554. val context = ValidationContext()
  555. try {
  556. if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
  557. return@setAssertionValidator result
  558. }
  559. } catch (e: Exception) {
  560. return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
  561. }
  562. result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
  563. }
  564. ----
  565. ======
  566. [NOTE]
  567. While recommended, it's not necessary to call ``OpenSaml4AuthenticationProvider``'s default assertion validator.
  568. 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.
  569. If you are using xref:servlet/saml2/opensaml.adoc[OpenSAML 5], then we have a simpler way using `OpenSaml5AuthenticationProvider.AssertionValidator`:
  570. [tabs]
  571. ======
  572. Java::
  573. +
  574. [source,java,role="primary"]
  575. ----
  576. OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
  577. OneTimeUseConditionValidator validator = ...;
  578. AssertionValidator assertionValidator = AssertionValidator.builder()
  579. .conditionValidators((c) -> c.add(validator)).build();
  580. provider.setAssertionValidator(assertionValidator);
  581. ----
  582. Kotlin::
  583. +
  584. [source,kotlin,role="secondary"]
  585. ----
  586. val provider = OpenSaml5AuthenticationProvider()
  587. val validator: OneTimeUseConditionValidator = ...;
  588. val assertionValidator = AssertionValidator.builder()
  589. .conditionValidators { add(validator) }.build()
  590. provider.setAssertionValidator(assertionValidator)
  591. ----
  592. ======
  593. You can use this same builder to remove validators that you don't want to use like so:
  594. [tabs]
  595. ======
  596. Java::
  597. +
  598. [source,java,role="primary"]
  599. ----
  600. OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
  601. AssertionValidator assertionValidator = AssertionValidator.builder()
  602. .conditionValidators((c) -> c.removeIf(AudienceRestrictionValidator.class::isInstance)).build();
  603. provider.setAssertionValidator(assertionValidator);
  604. ----
  605. Kotlin::
  606. +
  607. [source,kotlin,role="secondary"]
  608. ----
  609. val provider = new OpenSaml5AuthenticationProvider()
  610. val assertionValidator = AssertionValidator.builder()
  611. .conditionValidators {
  612. c: List<ConditionValidator> -> c.removeIf { it is AudienceRestrictionValidator }
  613. }.build()
  614. provider.setAssertionValidator(assertionValidator)
  615. ----
  616. ======
  617. [[servlet-saml2login-opensamlauthenticationprovider-decryption]]
  618. == Customizing Decryption
  619. Spring Security decrypts `<saml2:EncryptedAssertion>`, `<saml2:EncryptedAttribute>`, and `<saml2:EncryptedID>` elements automatically by using the decryption xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-credentials[`Saml2X509Credential` instances] registered in the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`].
  620. `OpenSaml4AuthenticationProvider` exposes xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[two decryption strategies].
  621. The response decrypter is for decrypting encrypted elements of the `<saml2:Response>`, like `<saml2:EncryptedAssertion>`.
  622. The assertion decrypter is for decrypting encrypted elements of the `<saml2:Assertion>`, like `<saml2:EncryptedAttribute>` and `<saml2:EncryptedID>`.
  623. You can replace ``OpenSaml4AuthenticationProvider``'s default decryption strategy with your own.
  624. For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>`, you can use it instead like so:
  625. [tabs]
  626. ======
  627. Java::
  628. +
  629. [source,java,role="primary"]
  630. ----
  631. MyDecryptionService decryptionService = ...;
  632. OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
  633. provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
  634. ----
  635. Kotlin::
  636. +
  637. [source,kotlin,role="secondary"]
  638. ----
  639. val decryptionService: MyDecryptionService = ...
  640. val provider = OpenSaml4AuthenticationProvider()
  641. provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
  642. ----
  643. ======
  644. If you are also decrypting individual elements in a `<saml2:Assertion>`, you can customize the assertion decrypter, too:
  645. [tabs]
  646. ======
  647. Java::
  648. +
  649. [source,java,role="primary"]
  650. ----
  651. provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
  652. ----
  653. Kotlin::
  654. +
  655. [source,kotlin,role="secondary"]
  656. ----
  657. provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
  658. ----
  659. ======
  660. NOTE: There are two separate decrypters since assertions can be signed separately from responses.
  661. Trying to decrypt a signed assertion's elements before signature verification may invalidate the signature.
  662. If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter.
  663. [[servlet-saml2login-authenticationmanager-custom]]
  664. == Using a Custom Authentication Manager
  665. [[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]]
  666. Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication.
  667. This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2.0 Response XML data.
  668. [tabs]
  669. ======
  670. Java::
  671. +
  672. [source,java,role="primary"]
  673. ----
  674. @Configuration
  675. @EnableWebSecurity
  676. public class SecurityConfig {
  677. @Bean
  678. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  679. AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
  680. http
  681. .authorizeHttpRequests(authorize -> authorize
  682. .anyRequest().authenticated()
  683. )
  684. .saml2Login(saml2 -> saml2
  685. .authenticationManager(authenticationManager)
  686. )
  687. ;
  688. return http.build();
  689. }
  690. }
  691. ----
  692. Kotlin::
  693. +
  694. [source,kotlin,role="secondary"]
  695. ----
  696. @Configuration
  697. @EnableWebSecurity
  698. open class SecurityConfig {
  699. @Bean
  700. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  701. val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
  702. http {
  703. authorizeRequests {
  704. authorize(anyRequest, authenticated)
  705. }
  706. saml2Login {
  707. authenticationManager = customAuthenticationManager
  708. }
  709. }
  710. return http.build()
  711. }
  712. }
  713. ----
  714. ======
  715. [[servlet-saml2login-authenticatedprincipal]]
  716. == Using `Saml2AuthenticatedPrincipal`
  717. With the relying party correctly configured for a given asserting party, it's ready to accept assertions.
  718. Once the relying party validates an assertion, the result is a `Saml2Authentication` with a `Saml2AuthenticatedPrincipal`.
  719. This means that you can access the principal in your controller like so:
  720. [tabs]
  721. ======
  722. Java::
  723. +
  724. [source,java,role="primary"]
  725. ----
  726. @Controller
  727. public class MainController {
  728. @GetMapping("/")
  729. public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
  730. String email = principal.getFirstAttribute("email");
  731. model.setAttribute("email", email);
  732. return "index";
  733. }
  734. }
  735. ----
  736. Kotlin::
  737. +
  738. [source,kotlin,role="secondary"]
  739. ----
  740. @Controller
  741. class MainController {
  742. @GetMapping("/")
  743. fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
  744. val email = principal.getFirstAttribute<String>("email")
  745. model.setAttribute("email", email)
  746. return "index"
  747. }
  748. }
  749. ----
  750. ======
  751. [TIP]
  752. 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.
  753. `getFirstAttribute` is quite handy when you know that there is only one value.