authentication.adoc 15 KB

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