saml2.adoc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. = Saml 2.0 Migrations
  2. == Use OpenSAML 5 By Default
  3. OpenSAML 4.x is no longer supported by the OpenSAML team.
  4. As such, Spring Security will default to using its `OpenSaml5` components in all cases.
  5. If you want to see how well your application will respond to this, do the following:
  6. 1. Update your OpenSAML dependencies to 5.x
  7. 2. If you are constructing an `OpenSaml4XXX` Spring Security component, change it to `OpenSaml5`.
  8. If you cannot opt-in, then add the `opensaml-saml-api` and `opensaml-saml-impl` 4.x dependencies and exclude the 5.x dependencies from `spring-security-saml2-service-provider`.
  9. == Continue Filter Chain When No Relying Party Found
  10. In Spring Security 6, `Saml2WebSsoAuthenticationFilter` throws an exception when the request URI matches, but no relying party registration is found.
  11. There are a number of cases when an application would not consider this an error situation.
  12. For example, this filter doesn't know how the `AuthorizationFilter` will respond to a missing relying party.
  13. In some cases it may be allowable.
  14. In other cases, you may want your `AuthenticationEntryPoint` to be invoked, which would happen if this filter were to allow the request to continue to the `AuthorizationFilter`.
  15. To improve this filter's flexibility, in Spring Security 7 it will continue the filter chain when there is no relying party registration found instead of throwing an exception.
  16. For many applications, the only notable change will be that your `authenticationEntryPoint` will be invoked if the relying party registration cannot be found.
  17. When you have only one asserting party, this means by default a new authentication request will be built and sent back to the asserting party, which may cause a "Too Many Redirects" loop.
  18. To see if you are affected in this way, you can prepare for this change in 6 by setting the following property in `Saml2WebSsoAuthenticationFilter`:
  19. [tabs]
  20. ======
  21. Java::
  22. +
  23. [source,java,role="primary"]
  24. ----
  25. http
  26. .saml2Login((saml2) -> saml2
  27. .withObjectPostProcessor(new ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter>() {
  28. @Override
  29. public Saml2WebSsoAuthenticationFilter postProcess(Saml2WebSsoAuthenticationFilter filter) {
  30. filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true);
  31. return filter;
  32. }
  33. })
  34. )
  35. ----
  36. Kotlin::
  37. +
  38. [source,kotlin,role="secondary"]
  39. ----
  40. http {
  41. saml2Login { }
  42. withObjectPostProcessor(
  43. object : ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter?>() {
  44. override fun postProcess(filter: Saml2WebSsoAuthenticationFilter): Saml2WebSsoAuthenticationFilter {
  45. filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true)
  46. return filter
  47. }
  48. })
  49. }
  50. ----
  51. Xml::
  52. +
  53. [source,xml,role="secondary"]
  54. ----
  55. <b:bean id="saml2PostProcessor" class="org.example.MySaml2WebSsoAuthenticationFilterBeanPostProcessor"/>
  56. ----
  57. ======
  58. == Validate Response After Validating Assertions
  59. In Spring Security 6, the order of authenticating a `<saml2:Response>` is as follows:
  60. 1. Verify the Response Signature, if any
  61. 2. Decrypt the Response
  62. 3. Validate Response attributes, like Destination and Issuer
  63. 4. For each assertion, verify the signature, decrypt, and then validate its fields
  64. 5. Check to ensure that the response has at least one assertion with a name field
  65. This ordering sometimes poses challenges since some response validation is being done in Step 3 and some in Step 5.
  66. Specifically, this poses a chellenge when an application doesn't have a name field and doesn't need it to be validated.
  67. In Spring Security 7, this is simplified by moving response validation to after assertion validation and combining the two separate validation steps 3 and 5.
  68. When this is complete, response validation will no longer check for the existence of the `NameID` attribute and rely on ``ResponseAuthenticationConverter``s to do this.
  69. This will add support ``ResponseAuthenticationConverter``s that don't use the `NameID` element in their `Authentication` instance and so don't need it validated.
  70. To opt-in to this behavior in advance, use `OpenSaml5AuthenticationProvider#setValidateResponseAfterAssertions` to `true` like so:
  71. [tabs]
  72. ======
  73. Java::
  74. +
  75. [source,java,role="primary"]
  76. ----
  77. OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
  78. provider.setValidateResponseAfterAssertions(true);
  79. ----
  80. Kotlin::
  81. +
  82. [source,kotlin,role="secondary"]
  83. ----
  84. val provider = OpenSaml5AuthenticationProvider()
  85. provider.setValidateResponseAfterAssertions(true)
  86. ----
  87. ======
  88. This will change the authentication steps as follows:
  89. 1. Verify the Response Signature, if any
  90. 2. Decrypt the Response
  91. 3. For each assertion, verify the signature, decrypt, and then validate its fields
  92. 4. Validate Response attributes, like Destination and Issuer
  93. Note that if you have a custom response authentication converter, then you are now responsible to check if the `NameID` element exists in the event that you need it.
  94. Alternatively to updating your response authentication converter, you can specify a custom `ResponseValidator` that adds back in the check for the `NameID` element as follows:
  95. [tabs]
  96. ======
  97. Java::
  98. +
  99. [source,java,role="primary"]
  100. ----
  101. OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
  102. provider.setValidateResponseAfterAssertions(true);
  103. ResponseValidator responseValidator = ResponseValidator.withDefaults((responseToken) -> {
  104. Response response = responseToken.getResponse();
  105. Assertion assertion = CollectionUtils.firstElement(response.getAssertions());
  106. Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
  107. "Assertion [" + firstAssertion.getID() + "] is missing a subject");
  108. Saml2ResponseValidationResult failed = Saml2ResponseValidationResult.failure(error);
  109. if (assertion.getSubject() == null) {
  110. return failed;
  111. }
  112. if (assertion.getSubject().getNameID() == null) {
  113. return failed;
  114. }
  115. if (assertion.getSubject().getNameID().getValue() == null) {
  116. return failed;
  117. }
  118. return Saml2ResponseValidationResult.success();
  119. });
  120. provider.setResponseValidator(responseValidator);
  121. ----
  122. Kotlin::
  123. +
  124. [source,kotlin,role="secondary"]
  125. ----
  126. val provider = OpenSaml5AuthenticationProvider()
  127. provider.setValidateResponseAfterAssertions(true)
  128. val responseValidator = ResponseValidator.withDefaults { responseToken: ResponseToken ->
  129. val response = responseToken.getResponse()
  130. val assertion = CollectionUtils.firstElement(response.getAssertions())
  131. val error = Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
  132. "Assertion [" + firstAssertion.getID() + "] is missing a subject")
  133. val failed = Saml2ResponseValidationResult.failure(error)
  134. if (assertion.getSubject() == null) {
  135. return@withDefaults failed
  136. }
  137. if (assertion.getSubject().getNameID() == null) {
  138. return@withDefaults failed
  139. }
  140. if (assertion.getSubject().getNameID().getValue() == null) {
  141. return@withDefaults failed
  142. }
  143. return@withDefaults Saml2ResponseValidationResult.success()
  144. }
  145. provider.setResponseValidator(responseValidator)
  146. ----
  147. ======
  148. == `RelyingPartyRegistration` Improvements
  149. `RelyingPartyRegistration` links metadata from a relying party to metadata from an asserting party.
  150. To prepare for some improvements to the API, please take the following steps:
  151. 1. If you are mutating a registration by using `RelyingPartyRegistration#withRelyingPartyRegistration`, instead call `RelyingPartyRegistration#mutate`
  152. 2. If you are providing or retrieving `AssertingPartyDetails`, use `getAssertingPartyMetadata` or `withAssertingPartyMetadata` instead.
  153. == `OpenSaml5AuthenticationProvider` Improvements
  154. Spring Security 7 will remove a handful of static factories from `OpenSaml5AuthenticationProvider` in favor of inner classes.
  155. These inner classes simplify customization of the response validator, the assertion validator, and the response authentication converter.
  156. === Response Validation
  157. Instead of doing:
  158. [tabs]
  159. ======
  160. Java::
  161. +
  162. [source,java,role="primary"]
  163. ----
  164. @Bean
  165. OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
  166. OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
  167. saml2.setResponseValidator((responseToken) -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
  168. .andThen((result) -> result
  169. .concat(myCustomValidator.convert(responseToken))
  170. ));
  171. return saml2;
  172. }
  173. ----
  174. Kotlin::
  175. +
  176. [source,kotlin,role="secondary"]
  177. ----
  178. @Bean
  179. fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
  180. val saml2 = OpenSaml5AuthenticationProvider()
  181. saml2.setResponseValidator { responseToken -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
  182. .andThen { result -> result
  183. .concat(myCustomValidator.convert(responseToken))
  184. }
  185. }
  186. return saml2
  187. }
  188. ----
  189. ======
  190. use `OpenSaml5AuthenticationProvider.ResponseValidator`:
  191. [tabs]
  192. ======
  193. Java::
  194. +
  195. [source,java,role="primary"]
  196. ----
  197. @Bean
  198. OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
  199. OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
  200. saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator));
  201. return saml2;
  202. }
  203. ----
  204. Kotlin::
  205. +
  206. [source,kotlin,role="secondary"]
  207. ----
  208. @Bean
  209. fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
  210. val saml2 = OpenSaml5AuthenticationProvider()
  211. saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator))
  212. return saml2
  213. }
  214. ----
  215. ======
  216. === Assertion Validation
  217. Instead of doing:
  218. [tabs]
  219. ======
  220. Java::
  221. +
  222. [source,java,role="primary"]
  223. ----
  224. @Bean
  225. OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
  226. OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
  227. authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider
  228. .createDefaultAssertionValidatorWithParameters(assertionToken -> {
  229. Map<String, Object> params = new HashMap<>();
  230. params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
  231. // ... other validation parameters
  232. return new ValidationContext(params);
  233. })
  234. );
  235. return saml2;
  236. }
  237. ----
  238. Kotlin::
  239. +
  240. [source,kotlin,role="secondary"]
  241. ----
  242. @Bean
  243. fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
  244. val saml2 = OpenSaml5AuthenticationProvider()
  245. authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider
  246. .createDefaultAssertionValidatorWithParameters { ->
  247. val params = HashMap<String, Object>()
  248. params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis())
  249. // ... other validation parameters
  250. return ValidationContext(params)
  251. }
  252. )
  253. return saml2
  254. }
  255. ----
  256. ======
  257. use `OpenSaml5AuthenticationProvider.AssertionValidator`:
  258. [tabs]
  259. ======
  260. Java::
  261. +
  262. [source,java,role="primary"]
  263. ----
  264. @Bean
  265. OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
  266. OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
  267. Duration tenMinutes = Duration.ofMinutes(10);
  268. authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build());
  269. return saml2;
  270. }
  271. ----
  272. Kotlin::
  273. +
  274. [source,kotlin,role="secondary"]
  275. ----
  276. @Bean
  277. fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
  278. val saml2 = OpenSaml5AuthenticationProvider()
  279. val tenMinutes = Duration.ofMinutes(10)
  280. authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build())
  281. return saml2
  282. }
  283. ----
  284. ======
  285. == Response Authentication Converter
  286. Instead of doing:
  287. [tabs]
  288. ======
  289. Java::
  290. +
  291. [source,java,role="primary"]
  292. ----
  293. @Bean
  294. Converter<ResponseToken, Saml2Authentication> authenticationConverter() {
  295. return (responseToken) -> {
  296. Saml2Authentication authentication = OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter()
  297. .convert(responseToken);
  298. // ... work with OpenSAML's Assertion object to extract the principal
  299. return new Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities());
  300. };
  301. }
  302. ----
  303. Kotlin::
  304. +
  305. [source,kotlin,role="secondary"]
  306. ----
  307. @Bean
  308. fun authenticationConverter(): Converter<ResponseToken, Saml2Authentication> {
  309. return { responseToken ->
  310. val authentication =
  311. OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter().convert(responseToken)
  312. // ... work with OpenSAML's Assertion object to extract the principal
  313. return Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities())
  314. }
  315. }
  316. ----
  317. ======
  318. use `OpenSaml5AuthenticationProvider.ResponseAuthenticationConverter`:
  319. [tabs]
  320. ======
  321. Java::
  322. +
  323. [source,java,role="primary"]
  324. ----
  325. @Bean
  326. ResponseAuthenticationConverter authenticationConverter() {
  327. ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
  328. authenticationConverter.setPrincipalNameConverter((assertion) -> {
  329. // ... work with OpenSAML's Assertion object to extract the principal
  330. });
  331. return authenticationConverter;
  332. }
  333. ----
  334. Kotlin::
  335. +
  336. [source,kotlin,role="secondary"]
  337. ----
  338. @Bean
  339. fun authenticationConverter(): ResponseAuthenticationConverter {
  340. val authenticationConverter = ResponseAuthenticationConverter()
  341. authenticationConverter.setPrincipalNameConverter { assertion ->
  342. // ... work with OpenSAML's Assertion object to extract the principal
  343. }
  344. return authenticationConverter
  345. }
  346. ----
  347. ======