|
37 | 37 | import org.opensaml.saml.saml2.core.Assertion;
|
38 | 38 | import org.opensaml.saml.saml2.core.AuthnRequest;
|
39 | 39 |
|
| 40 | +import org.springframework.beans.factory.BeanCreationException; |
40 | 41 | import org.springframework.beans.factory.annotation.Autowired;
|
41 | 42 | import org.springframework.context.ConfigurableApplicationContext;
|
42 | 43 | import org.springframework.context.annotation.Bean;
|
|
97 | 98 | import org.springframework.web.util.UriComponentsBuilder;
|
98 | 99 |
|
99 | 100 | import static org.assertj.core.api.Assertions.assertThat;
|
| 101 | +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
100 | 102 | import static org.mockito.ArgumentMatchers.any;
|
101 | 103 | import static org.mockito.ArgumentMatchers.anyString;
|
102 | 104 | import static org.mockito.BDDMockito.given;
|
@@ -124,6 +126,8 @@ public class Saml2LoginConfigurerTests {
|
124 | 126 |
|
125 | 127 | private static final String SIGNED_RESPONSE = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9ycC5leGFtcGxlLm9yZy9hY3MiIElEPSJfYzE3MzM2YTAtNTM1My00MTQ5LWI3MmMtMDNkOWY5YWYzMDdlIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDgtMDRUMjI6MDQ6NDUuMDE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KPGRzOlNpZ25lZEluZm8+CjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CjxkczpSZWZlcmVuY2UgVVJJPSIjX2MxNzMzNmEwLTUzNTMtNDE0OS1iNzJjLTAzZDlmOWFmMzA3ZSI+CjxkczpUcmFuc2Zvcm1zPgo8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgo8L2RzOlRyYW5zZm9ybXM+CjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz4KPGRzOkRpZ2VzdFZhbHVlPjYzTmlyenFzaDVVa0h1a3NuRWUrM0hWWU5aYWFsQW1OQXFMc1lGMlRuRDA9PC9kczpEaWdlc3RWYWx1ZT4KPC9kczpSZWZlcmVuY2U+CjwvZHM6U2lnbmVkSW5mbz4KPGRzOlNpZ25hdHVyZVZhbHVlPgpLMVlvWWJVUjBTclY4RTdVMkhxTTIvZUNTOTNoV25mOExnNnozeGZWMUlyalgzSXhWYkNvMVlYcnRBSGRwRVdvYTJKKzVOMmFNbFBHJiMxMzsKN2VpbDBZRC9xdUVRamRYbTNwQTBjZmEvY25pa2RuKzVhbnM0ZWQwanU1amo2dkpvZ2w2Smt4Q25LWUpwTU9HNzhtampmb0phengrWCYjMTM7CkM2NktQVStBYUdxeGVwUEQ1ZlhRdTFKSy9Jb3lBaitaa3k4Z2Jwc3VyZHFCSEJLRWxjdnVOWS92UGY0OGtBeFZBKzdtRGhNNUMvL1AmIzEzOwp0L084Y3NZYXB2UjZjdjZrdk45QXZ1N3FRdm9qVk1McHVxZWNJZDJwTUVYb0NSSnE2Nkd4MStNTUVPeHVpMWZZQlRoMEhhYjRmK3JyJiMxMzsKOEY2V1NFRC8xZllVeHliRkJqZ1Q4d2lEWHFBRU8wSVY4ZWRQeEE9PQo8L2RzOlNpZ25hdHVyZVZhbHVlPgo8L2RzOlNpZ25hdHVyZT48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iQWUzZjQ5OGI4LTliMTctNDA3OC05ZDM1LTg2YTA4NDA4NDk5NSIgSXNzdWVJbnN0YW50PSIyMDIwLTA4LTA0VDIyOjA0OjQ1LjA3N1oiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3Vlcj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEPnRlc3RAc2FtbC51c2VyPC9zYW1sMjpOYW1lSUQ+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90QmVmb3JlPSIyMDIwLTA4LTA0VDIxOjU5OjQ1LjA5MFoiIE5vdE9uT3JBZnRlcj0iMjA0MC0wNy0zMFQyMjowNTowNi4wODhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcnAuZXhhbXBsZS5vcmcvYWNzIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjAtMDgtMDRUMjE6NTk6NDUuMDgwWiIgTm90T25PckFmdGVyPSIyMDQwLTA3LTMwVDIyOjA1OjA2LjA4N1oiLz48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4=";
|
126 | 128 |
|
| 129 | + private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class); |
| 130 | + |
127 | 131 | @Autowired
|
128 | 132 | private ConfigurableApplicationContext context;
|
129 | 133 |
|
@@ -286,6 +290,53 @@ public void authenticateWhenCustomAuthnRequestRepositoryThenUses() throws Except
|
286 | 290 | verify(repository).removeAuthenticationRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
287 | 291 | }
|
288 | 292 |
|
| 293 | + @Test |
| 294 | + public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenValidates() { |
| 295 | + assertThatExceptionOfType(BeanCreationException.class) |
| 296 | + .isThrownBy(() -> this.spring.register(CustomLoginProcessingUrlDefaultAuthenticationConverter.class) |
| 297 | + .autowire()) |
| 298 | + .havingRootCause().isInstanceOf(IllegalStateException.class) |
| 299 | + .withMessage("loginProcessingUrl must contain {registrationId} path variable"); |
| 300 | + } |
| 301 | + |
| 302 | + @Test |
| 303 | + public void authenticateWhenCustomLoginProcessingUrlAndCustomAuthenticationConverterThenAuthenticate() |
| 304 | + throws Exception { |
| 305 | + this.spring.register(CustomLoginProcessingUrlCustomAuthenticationConverter.class).autowire(); |
| 306 | + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() |
| 307 | + .assertingPartyDetails((party) -> party.verificationX509Credentials( |
| 308 | + (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) |
| 309 | + .build(); |
| 310 | + String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); |
| 311 | + given(AUTHENTICATION_CONVERTER.convert(any(HttpServletRequest.class))) |
| 312 | + .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); |
| 313 | + // @formatter:off |
| 314 | + MockHttpServletRequestBuilder request = post("/my/custom/url").param("SAMLResponse", SIGNED_RESPONSE); |
| 315 | + // @formatter:on |
| 316 | + this.mvc.perform(request).andExpect(redirectedUrl("/")); |
| 317 | + verify(AUTHENTICATION_CONVERTER).convert(any(HttpServletRequest.class)); |
| 318 | + } |
| 319 | + |
| 320 | + @Test |
| 321 | + public void authenticateWhenCustomLoginProcessingUrlAndSaml2AuthenticationTokenConverterBeanThenAuthenticate() |
| 322 | + throws Exception { |
| 323 | + this.spring.register(CustomLoginProcessingUrlSaml2AuthenticationTokenConverterBean.class).autowire(); |
| 324 | + Saml2AuthenticationTokenConverter authenticationConverter = this.spring.getContext() |
| 325 | + .getBean(Saml2AuthenticationTokenConverter.class); |
| 326 | + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials() |
| 327 | + .assertingPartyDetails((party) -> party.verificationX509Credentials( |
| 328 | + (c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))) |
| 329 | + .build(); |
| 330 | + String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE)); |
| 331 | + given(authenticationConverter.convert(any(HttpServletRequest.class))) |
| 332 | + .willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response)); |
| 333 | + // @formatter:off |
| 334 | + MockHttpServletRequestBuilder request = post("/my/custom/url").param("SAMLResponse", SIGNED_RESPONSE); |
| 335 | + // @formatter:on |
| 336 | + this.mvc.perform(request).andExpect(redirectedUrl("/")); |
| 337 | + verify(authenticationConverter).convert(any(HttpServletRequest.class)); |
| 338 | + } |
| 339 | + |
289 | 340 | private void validateSaml2WebSsoAuthenticationFilterConfiguration() {
|
290 | 341 | // get the OpenSamlAuthenticationProvider
|
291 | 342 | Saml2WebSsoAuthenticationFilter filter = getSaml2SsoFilter(this.springSecurityFilterChain);
|
@@ -511,6 +562,65 @@ Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authent
|
511 | 562 |
|
512 | 563 | }
|
513 | 564 |
|
| 565 | + @EnableWebSecurity |
| 566 | + @Import(Saml2LoginConfigBeans.class) |
| 567 | + static class CustomLoginProcessingUrlDefaultAuthenticationConverter { |
| 568 | + |
| 569 | + @Bean |
| 570 | + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
| 571 | + // @formatter:off |
| 572 | + http |
| 573 | + .authorizeRequests((authz) -> authz.anyRequest().authenticated()) |
| 574 | + .saml2Login((saml2) -> saml2.loginProcessingUrl("/my/custom/url")); |
| 575 | + // @formatter:on |
| 576 | + return http.build(); |
| 577 | + } |
| 578 | + |
| 579 | + } |
| 580 | + |
| 581 | + @EnableWebSecurity |
| 582 | + @Import(Saml2LoginConfigBeans.class) |
| 583 | + static class CustomLoginProcessingUrlCustomAuthenticationConverter { |
| 584 | + |
| 585 | + @Bean |
| 586 | + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
| 587 | + // @formatter:off |
| 588 | + http |
| 589 | + .authorizeRequests((authz) -> authz.anyRequest().authenticated()) |
| 590 | + .saml2Login((saml2) -> saml2 |
| 591 | + .loginProcessingUrl("/my/custom/url") |
| 592 | + .authenticationConverter(AUTHENTICATION_CONVERTER) |
| 593 | + ); |
| 594 | + // @formatter:on |
| 595 | + return http.build(); |
| 596 | + } |
| 597 | + |
| 598 | + } |
| 599 | + |
| 600 | + @EnableWebSecurity |
| 601 | + @Import(Saml2LoginConfigBeans.class) |
| 602 | + static class CustomLoginProcessingUrlSaml2AuthenticationTokenConverterBean { |
| 603 | + |
| 604 | + private final Saml2AuthenticationTokenConverter authenticationConverter = mock( |
| 605 | + Saml2AuthenticationTokenConverter.class); |
| 606 | + |
| 607 | + @Bean |
| 608 | + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
| 609 | + // @formatter:off |
| 610 | + http |
| 611 | + .authorizeRequests((authz) -> authz.anyRequest().authenticated()) |
| 612 | + .saml2Login((saml2) -> saml2.loginProcessingUrl("/my/custom/url")); |
| 613 | + // @formatter:on |
| 614 | + return http.build(); |
| 615 | + } |
| 616 | + |
| 617 | + @Bean |
| 618 | + Saml2AuthenticationTokenConverter authenticationTokenConverter() { |
| 619 | + return this.authenticationConverter; |
| 620 | + } |
| 621 | + |
| 622 | + } |
| 623 | + |
514 | 624 | static class Saml2LoginConfigBeans {
|
515 | 625 |
|
516 | 626 | @Bean
|
|
0 commit comments