authentication.adoc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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. ====
  16. .Java
  17. [source,java,role="primary"]
  18. ----
  19. @Bean
  20. SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
  21. http
  22. // ...
  23. .saml2Login((saml2) -> saml2.filterProcessingUrl("/saml2/login/sso"))
  24. // ...
  25. return http.build();
  26. }
  27. ----
  28. .Kotlin
  29. [source,kotlin,role="secondary"]
  30. ----
  31. @Bean
  32. fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
  33. http {
  34. // ...
  35. .saml2Login {
  36. filterProcessingUrl = "/saml2/login/sso"
  37. }
  38. // ...
  39. }
  40. return http.build()
  41. }
  42. ----
  43. ====
  44. and:
  45. ====
  46. .Java
  47. [source,java,role="primary"]
  48. ----
  49. relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
  50. ----
  51. .Kotlin
  52. [source,kotlin,role="secondary"]
  53. ----
  54. relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
  55. ----
  56. ====
  57. [[relyingpartyregistrationresolver-apply]]
  58. == Changing `RelyingPartyRegistration` lookup
  59. By default, this converter will match against any associated `<saml2:AuthnRequest>` or any `registrationId` it finds in the URL.
  60. Or, if it cannot find one in either of those cases, then it attempts to look it up by the `<saml2:Response#Issuer>` element.
  61. There are a number of circumstances where you might need something more sophisticated, like if you are supporting `ARTIFACT` binding.
  62. In those cases, you can customize lookup through a custom `AuthenticationConverter`, which you can customize like so:
  63. ====
  64. .Java
  65. [source,java,role="primary"]
  66. ----
  67. @Bean
  68. SecurityFilterChain securityFilters(HttpSecurity http, AuthenticationConverter authenticationConverter) throws Exception {
  69. http
  70. // ...
  71. .saml2Login((saml2) -> saml2.authenticationConverter(authenticationConverter))
  72. // ...
  73. return http.build();
  74. }
  75. ----
  76. .Kotlin
  77. [source,kotlin,role="secondary"]
  78. ----
  79. @Bean
  80. fun securityFilters(val http: HttpSecurity, val converter: AuthenticationConverter): SecurityFilterChain {
  81. http {
  82. // ...
  83. .saml2Login {
  84. authenticationConverter = converter
  85. }
  86. // ...
  87. }
  88. return http.build()
  89. }
  90. ----
  91. ====
  92. [[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
  93. == Setting a Clock Skew
  94. It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized.
  95. For that reason, you can configure `OpenSaml4AuthenticationProvider` 's default assertion validator with some tolerance:
  96. ====
  97. .Java
  98. [source,java,role="primary"]
  99. ----
  100. @Configuration
  101. @EnableWebSecurity
  102. public class SecurityConfig {
  103. @Bean
  104. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  105. OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
  106. authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
  107. .createDefaultAssertionValidator(assertionToken -> {
  108. Map<String, Object> params = new HashMap<>();
  109. params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
  110. // ... other validation parameters
  111. return new ValidationContext(params);
  112. })
  113. );
  114. http
  115. .authorizeHttpRequests(authz -> authz
  116. .anyRequest().authenticated()
  117. )
  118. .saml2Login(saml2 -> saml2
  119. .authenticationManager(new ProviderManager(authenticationProvider))
  120. );
  121. return http.build();
  122. }
  123. }
  124. ----
  125. .Kotlin
  126. [source,kotlin,role="secondary"]
  127. ----
  128. @Configuration
  129. @EnableWebSecurity
  130. open class SecurityConfig {
  131. @Bean
  132. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  133. val authenticationProvider = OpenSaml4AuthenticationProvider()
  134. authenticationProvider.setAssertionValidator(
  135. OpenSaml4AuthenticationProvider
  136. .createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
  137. val params: MutableMap<String, Any> = HashMap()
  138. params[CLOCK_SKEW] =
  139. Duration.ofMinutes(10).toMillis()
  140. ValidationContext(params)
  141. })
  142. )
  143. http {
  144. authorizeRequests {
  145. authorize(anyRequest, authenticated)
  146. }
  147. saml2Login {
  148. authenticationManager = ProviderManager(authenticationProvider)
  149. }
  150. }
  151. return http.build()
  152. }
  153. }
  154. ----
  155. ====
  156. [[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
  157. == Coordinating with a `UserDetailsService`
  158. Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
  159. In that case, the response authentication converter can come in handy, as can be seen below:
  160. ====
  161. .Java
  162. [source,java,role="primary"]
  163. ----
  164. @Configuration
  165. @EnableWebSecurity
  166. public class SecurityConfig {
  167. @Autowired
  168. UserDetailsService userDetailsService;
  169. @Bean
  170. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  171. OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
  172. authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
  173. Saml2Authentication authentication = OpenSaml4AuthenticationProvider
  174. .createDefaultResponseAuthenticationConverter() <1>
  175. .convert(responseToken);
  176. Assertion assertion = responseToken.getResponse().getAssertions().get(0);
  177. String username = assertion.getSubject().getNameID().getValue();
  178. UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); <2>
  179. return MySaml2Authentication(userDetails, authentication); <3>
  180. });
  181. http
  182. .authorizeHttpRequests(authz -> authz
  183. .anyRequest().authenticated()
  184. )
  185. .saml2Login(saml2 -> saml2
  186. .authenticationManager(new ProviderManager(authenticationProvider))
  187. );
  188. return http.build();
  189. }
  190. }
  191. ----
  192. .Kotlin
  193. [source,kotlin,role="secondary"]
  194. ----
  195. @Configuration
  196. @EnableWebSecurity
  197. open class SecurityConfig {
  198. @Autowired
  199. var userDetailsService: UserDetailsService? = null
  200. @Bean
  201. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  202. val authenticationProvider = OpenSaml4AuthenticationProvider()
  203. authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
  204. val authentication = OpenSaml4AuthenticationProvider
  205. .createDefaultResponseAuthenticationConverter() <1>
  206. .convert(responseToken)
  207. val assertion: Assertion = responseToken.response.assertions[0]
  208. val username: String = assertion.subject.nameID.value
  209. val userDetails = userDetailsService!!.loadUserByUsername(username) <2>
  210. MySaml2Authentication(userDetails, authentication) <3>
  211. }
  212. http {
  213. authorizeRequests {
  214. authorize(anyRequest, authenticated)
  215. }
  216. saml2Login {
  217. authenticationManager = ProviderManager(authenticationProvider)
  218. }
  219. }
  220. return http.build()
  221. }
  222. }
  223. ----
  224. ====
  225. <1> First, call the default converter, which extracts attributes and authorities from the response
  226. <2> Second, call the xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[`UserDetailsService`] using the relevant information
  227. <3> Third, return a custom authentication that includes the user details
  228. [NOTE]
  229. It's not required to call `OpenSaml4AuthenticationProvider` 's default authentication converter.
  230. It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from ``AttributeStatement``s as well as the single `ROLE_USER` authority.
  231. [[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
  232. == Performing Additional Response Validation
  233. `OpenSaml4AuthenticationProvider` validates the `Issuer` and `Destination` values right after decrypting the `Response`.
  234. You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.
  235. For example, you can throw a custom exception with any additional information available in the `Response` object, like so:
  236. [source,java]
  237. ----
  238. OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
  239. provider.setResponseValidator((responseToken) -> {
  240. Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
  241. .createDefaultResponseValidator()
  242. .convert(responseToken)
  243. .concat(myCustomValidator.convert(responseToken));
  244. if (!result.getErrors().isEmpty()) {
  245. String inResponseTo = responseToken.getInResponseTo();
  246. throw new CustomSaml2AuthenticationException(result, inResponseTo);
  247. }
  248. return result;
  249. });
  250. ----
  251. == Performing Additional Assertion Validation
  252. `OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
  253. After verifying the signature, it will:
  254. 1. Validate `<AudienceRestriction>` and `<DelegationRestriction>` conditions
  255. 2. Validate ``<SubjectConfirmation>``s, expect for any IP address information
  256. To perform additional validation, you can configure your own assertion validator that delegates to `OpenSaml4AuthenticationProvider` 's default and then performs its own.
  257. [[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]]
  258. For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
  259. ====
  260. .Java
  261. [source,java,role="primary"]
  262. ----
  263. OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
  264. OneTimeUseConditionValidator validator = ...;
  265. provider.setAssertionValidator(assertionToken -> {
  266. Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
  267. .createDefaultAssertionValidator()
  268. .convert(assertionToken);
  269. Assertion assertion = assertionToken.getAssertion();
  270. OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
  271. ValidationContext context = new ValidationContext();
  272. try {
  273. if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
  274. return result;
  275. }
  276. } catch (Exception e) {
  277. return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
  278. }
  279. return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
  280. });
  281. ----
  282. .Kotlin
  283. [source,kotlin,role="secondary"]
  284. ----
  285. var provider = OpenSaml4AuthenticationProvider()
  286. var validator: OneTimeUseConditionValidator = ...
  287. provider.setAssertionValidator { assertionToken ->
  288. val result = OpenSaml4AuthenticationProvider
  289. .createDefaultAssertionValidator()
  290. .convert(assertionToken)
  291. val assertion: Assertion = assertionToken.assertion
  292. val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
  293. val context = ValidationContext()
  294. try {
  295. if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
  296. return@setAssertionValidator result
  297. }
  298. } catch (e: Exception) {
  299. return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
  300. }
  301. result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
  302. }
  303. ----
  304. ====
  305. [NOTE]
  306. While recommended, it's not necessary to call `OpenSaml4AuthenticationProvider` 's default assertion validator.
  307. 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.
  308. [[servlet-saml2login-opensamlauthenticationprovider-decryption]]
  309. == Customizing Decryption
  310. 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`].
  311. `OpenSaml4AuthenticationProvider` exposes xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[two decryption strategies].
  312. The response decrypter is for decrypting encrypted elements of the `<saml2:Response>`, like `<saml2:EncryptedAssertion>`.
  313. The assertion decrypter is for decrypting encrypted elements of the `<saml2:Assertion>`, like `<saml2:EncryptedAttribute>` and `<saml2:EncryptedID>`.
  314. You can replace `OpenSaml4AuthenticationProvider`'s default decryption strategy with your own.
  315. For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>`, you can use it instead like so:
  316. ====
  317. .Java
  318. [source,java,role="primary"]
  319. ----
  320. MyDecryptionService decryptionService = ...;
  321. OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
  322. provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
  323. ----
  324. .Kotlin
  325. [source,kotlin,role="secondary"]
  326. ----
  327. val decryptionService: MyDecryptionService = ...
  328. val provider = OpenSaml4AuthenticationProvider()
  329. provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
  330. ----
  331. ====
  332. If you are also decrypting individual elements in a `<saml2:Assertion>`, you can customize the assertion decrypter, too:
  333. ====
  334. .Java
  335. [source,java,role="primary"]
  336. ----
  337. provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
  338. ----
  339. .Kotlin
  340. [source,kotlin,role="secondary"]
  341. ----
  342. provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
  343. ----
  344. ====
  345. NOTE: There are two separate decrypters since assertions can be signed separately from responses.
  346. Trying to decrypt a signed assertion's elements before signature verification may invalidate the signature.
  347. If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter.
  348. [[servlet-saml2login-authenticationmanager-custom]]
  349. == Using a Custom Authentication Manager
  350. [[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]]
  351. Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication.
  352. This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2.0 Response XML data.
  353. ====
  354. .Java
  355. [source,java,role="primary"]
  356. ----
  357. @Configuration
  358. @EnableWebSecurity
  359. public class SecurityConfig {
  360. @Bean
  361. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  362. AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
  363. http
  364. .authorizeHttpRequests(authorize -> authorize
  365. .anyRequest().authenticated()
  366. )
  367. .saml2Login(saml2 -> saml2
  368. .authenticationManager(authenticationManager)
  369. )
  370. ;
  371. return http.build();
  372. }
  373. }
  374. ----
  375. .Kotlin
  376. [source,kotlin,role="secondary"]
  377. ----
  378. @Configuration
  379. @EnableWebSecurity
  380. open class SecurityConfig {
  381. @Bean
  382. open fun filterChain(http: HttpSecurity): SecurityFilterChain {
  383. val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
  384. http {
  385. authorizeRequests {
  386. authorize(anyRequest, authenticated)
  387. }
  388. saml2Login {
  389. authenticationManager = customAuthenticationManager
  390. }
  391. }
  392. return http.build()
  393. }
  394. }
  395. ----
  396. ====
  397. [[servlet-saml2login-authenticatedprincipal]]
  398. == Using `Saml2AuthenticatedPrincipal`
  399. With the relying party correctly configured for a given asserting party, it's ready to accept assertions.
  400. Once the relying party validates an assertion, the result is a `Saml2Authentication` with a `Saml2AuthenticatedPrincipal`.
  401. This means that you can access the principal in your controller like so:
  402. ====
  403. .Java
  404. [source,java,role="primary"]
  405. ----
  406. @Controller
  407. public class MainController {
  408. @GetMapping("/")
  409. public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
  410. String email = principal.getFirstAttribute("email");
  411. model.setAttribute("email", email);
  412. return "index";
  413. }
  414. }
  415. ----
  416. .Kotlin
  417. [source,kotlin,role="secondary"]
  418. ----
  419. @Controller
  420. class MainController {
  421. @GetMapping("/")
  422. fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
  423. val email = principal.getFirstAttribute<String>("email")
  424. model.setAttribute("email", email)
  425. return "index"
  426. }
  427. }
  428. ----
  429. ====
  430. [TIP]
  431. 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.
  432. `getFirstAttribute` is quite handy when you know that there is only one value.