Skip to content

Commit 816e847

Browse files
Allow SAML 2.0 loginProcessingURL without registrationId
Closes gh-10176
1 parent 1f919bc commit 816e847

File tree

4 files changed

+125
-19
lines changed

4 files changed

+125
-19
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,19 @@ public Saml2LoginConfigurer<B> loginPage(String loginPage) {
172172
return this;
173173
}
174174

175+
/**
176+
* Specifies the URL to validate the credentials. If specified a custom URL, consider
177+
* specifying a custom {@link AuthenticationConverter} via
178+
* {@link #authenticationConverter(AuthenticationConverter)}, since the default
179+
* {@link AuthenticationConverter} implementation relies on the
180+
* <code>{registrationId}</code> path variable to be present in the URL
181+
* @param loginProcessingUrl the URL to validate the credentials
182+
* @return the {@link Saml2LoginConfigurer} for additional customization
183+
* @see Saml2WebSsoAuthenticationFilter#DEFAULT_FILTER_PROCESSES_URI
184+
*/
175185
@Override
176186
public Saml2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) {
177187
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty");
178-
Assert.state(loginProcessingUrl.contains("{registrationId}"), "{registrationId} path variable is required");
179188
this.loginProcessingUrl = loginProcessingUrl;
180189
return this;
181190
}
@@ -254,6 +263,8 @@ public void configure(B http) throws Exception {
254263

255264
private AuthenticationConverter getAuthenticationConverter(B http) {
256265
if (this.authenticationConverter == null) {
266+
Assert.state(this.loginProcessingUrl.contains("{registrationId}"),
267+
"loginProcessingUrl must contain {registrationId} path variable");
257268
return new Saml2AuthenticationTokenConverter(
258269
new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository));
259270
}

config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.opensaml.saml.saml2.core.Assertion;
3838
import org.opensaml.saml.saml2.core.AuthnRequest;
3939

40+
import org.springframework.beans.factory.BeanCreationException;
4041
import org.springframework.beans.factory.annotation.Autowired;
4142
import org.springframework.context.ConfigurableApplicationContext;
4243
import org.springframework.context.annotation.Bean;
@@ -49,6 +50,7 @@
4950
import org.springframework.security.authentication.AuthenticationProvider;
5051
import org.springframework.security.authentication.AuthenticationServiceException;
5152
import org.springframework.security.authentication.ProviderManager;
53+
import org.springframework.security.config.Customizer;
5254
import org.springframework.security.config.annotation.ObjectPostProcessor;
5355
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
5456
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -77,7 +79,9 @@
7779
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
7880
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
7981
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver;
82+
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
8083
import org.springframework.security.web.FilterChainProxy;
84+
import org.springframework.security.web.SecurityFilterChain;
8185
import org.springframework.security.web.authentication.AuthenticationConverter;
8286
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
8387
import org.springframework.security.web.context.HttpRequestResponseHolder;
@@ -91,6 +95,7 @@
9195
import org.springframework.web.util.UriComponentsBuilder;
9296

9397
import static org.assertj.core.api.Assertions.assertThat;
98+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
9499
import static org.mockito.ArgumentMatchers.any;
95100
import static org.mockito.ArgumentMatchers.anyString;
96101
import static org.mockito.BDDMockito.given;
@@ -117,6 +122,8 @@ public class Saml2LoginConfigurerTests {
117122

118123
private static final String SIGNED_RESPONSE = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9ycC5leGFtcGxlLm9yZy9hY3MiIElEPSJfYzE3MzM2YTAtNTM1My00MTQ5LWI3MmMtMDNkOWY5YWYzMDdlIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDgtMDRUMjI6MDQ6NDUuMDE2WiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KPGRzOlNpZ25lZEluZm8+CjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CjxkczpSZWZlcmVuY2UgVVJJPSIjX2MxNzMzNmEwLTUzNTMtNDE0OS1iNzJjLTAzZDlmOWFmMzA3ZSI+CjxkczpUcmFuc2Zvcm1zPgo8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz4KPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgo8L2RzOlRyYW5zZm9ybXM+CjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz4KPGRzOkRpZ2VzdFZhbHVlPjYzTmlyenFzaDVVa0h1a3NuRWUrM0hWWU5aYWFsQW1OQXFMc1lGMlRuRDA9PC9kczpEaWdlc3RWYWx1ZT4KPC9kczpSZWZlcmVuY2U+CjwvZHM6U2lnbmVkSW5mbz4KPGRzOlNpZ25hdHVyZVZhbHVlPgpLMVlvWWJVUjBTclY4RTdVMkhxTTIvZUNTOTNoV25mOExnNnozeGZWMUlyalgzSXhWYkNvMVlYcnRBSGRwRVdvYTJKKzVOMmFNbFBHJiMxMzsKN2VpbDBZRC9xdUVRamRYbTNwQTBjZmEvY25pa2RuKzVhbnM0ZWQwanU1amo2dkpvZ2w2Smt4Q25LWUpwTU9HNzhtampmb0phengrWCYjMTM7CkM2NktQVStBYUdxeGVwUEQ1ZlhRdTFKSy9Jb3lBaitaa3k4Z2Jwc3VyZHFCSEJLRWxjdnVOWS92UGY0OGtBeFZBKzdtRGhNNUMvL1AmIzEzOwp0L084Y3NZYXB2UjZjdjZrdk45QXZ1N3FRdm9qVk1McHVxZWNJZDJwTUVYb0NSSnE2Nkd4MStNTUVPeHVpMWZZQlRoMEhhYjRmK3JyJiMxMzsKOEY2V1NFRC8xZllVeHliRkJqZ1Q4d2lEWHFBRU8wSVY4ZWRQeEE9PQo8L2RzOlNpZ25hdHVyZVZhbHVlPgo8L2RzOlNpZ25hdHVyZT48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iQWUzZjQ5OGI4LTliMTctNDA3OC05ZDM1LTg2YTA4NDA4NDk5NSIgSXNzdWVJbnN0YW50PSIyMDIwLTA4LTA0VDIyOjA0OjQ1LjA3N1oiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3Vlcj5hcC1lbnRpdHktaWQ8L3NhbWwyOklzc3Vlcj48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEPnRlc3RAc2FtbC51c2VyPC9zYW1sMjpOYW1lSUQ+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90QmVmb3JlPSIyMDIwLTA4LTA0VDIxOjU5OjQ1LjA5MFoiIE5vdE9uT3JBZnRlcj0iMjA0MC0wNy0zMFQyMjowNTowNi4wODhaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcnAuZXhhbXBsZS5vcmcvYWNzIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMjAtMDgtMDRUMjE6NTk6NDUuMDgwWiIgTm90T25PckFmdGVyPSIyMDQwLTA3LTMwVDIyOjA1OjA2LjA4N1oiLz48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4=";
119124

125+
private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class);
126+
120127
@Autowired
121128
private ConfigurableApplicationContext context;
122129

@@ -230,6 +237,33 @@ public void authenticateWithInvalidDeflatedSAMLResponseThenFailureHandlerUses()
230237
assertThat(exception.getCause()).isInstanceOf(IOException.class);
231238
}
232239

240+
@Test
241+
public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenValidates() {
242+
assertThatExceptionOfType(BeanCreationException.class)
243+
.isThrownBy(() -> this.spring.register(CustomLoginProcessingUrlDefaultAuthenticationConverter.class)
244+
.autowire())
245+
.havingRootCause().isInstanceOf(IllegalStateException.class)
246+
.withMessage("loginProcessingUrl must contain {registrationId} path variable");
247+
}
248+
249+
@Test
250+
public void authenticateWhenCustomLoginProcessingUrlAndCustomAuthenticationConverterThenAuthenticate()
251+
throws Exception {
252+
this.spring.register(CustomLoginProcessingUrlCustomAuthenticationConverter.class).autowire();
253+
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials()
254+
.assertingPartyDetails((party) -> party.verificationX509Credentials(
255+
(c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
256+
.build();
257+
String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE));
258+
given(AUTHENTICATION_CONVERTER.convert(any(HttpServletRequest.class)))
259+
.willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response));
260+
// @formatter:off
261+
MockHttpServletRequestBuilder request = post("/my/custom/url").param("SAMLResponse", SIGNED_RESPONSE);
262+
// @formatter:on
263+
this.mvc.perform(request).andExpect(redirectedUrl("/"));
264+
verify(AUTHENTICATION_CONVERTER).convert(any(HttpServletRequest.class));
265+
}
266+
233267
private void validateSaml2WebSsoAuthenticationFilterConfiguration() {
234268
// get the OpenSamlAuthenticationProvider
235269
Saml2WebSsoAuthenticationFilter filter = getSaml2SsoFilter(this.springSecurityFilterChain);
@@ -337,10 +371,10 @@ static class CustomAuthenticationRequestContextResolver extends WebSecurityConfi
337371
protected void configure(HttpSecurity http) throws Exception {
338372
// @formatter:off
339373
http
340-
.authorizeRequests((authz) -> authz
341-
.anyRequest().authenticated()
342-
)
343-
.saml2Login(withDefaults());
374+
.authorizeRequests((authz) -> authz
375+
.anyRequest().authenticated()
376+
)
377+
.saml2Login(withDefaults());
344378
// @formatter:on
345379
}
346380

@@ -359,11 +393,11 @@ static class CustomAuthenticationRequestContextConverterResolver extends WebSecu
359393
protected void configure(HttpSecurity http) throws Exception {
360394
// @formatter:off
361395
http
362-
.authorizeRequests((authz) -> authz
363-
.anyRequest().authenticated()
364-
)
365-
.saml2Login((saml2) -> {
366-
});
396+
.authorizeRequests((authz) -> authz
397+
.anyRequest().authenticated()
398+
)
399+
.saml2Login((saml2) -> {
400+
});
367401
// @formatter:on
368402
}
369403

@@ -395,6 +429,62 @@ protected void configure(HttpSecurity http) throws Exception {
395429

396430
}
397431

432+
@EnableWebSecurity
433+
@Import(Saml2LoginConfigBeans.class)
434+
static class CustomAuthenticationConverterBean {
435+
436+
private final Saml2AuthenticationTokenConverter authenticationConverter = mock(
437+
Saml2AuthenticationTokenConverter.class);
438+
439+
@Bean
440+
SecurityFilterChain app(HttpSecurity http) throws Exception {
441+
http.authorizeHttpRequests((authz) -> authz.anyRequest().authenticated())
442+
.saml2Login(Customizer.withDefaults());
443+
return http.build();
444+
}
445+
446+
@Bean
447+
Saml2AuthenticationTokenConverter authenticationConverter() {
448+
return this.authenticationConverter;
449+
}
450+
451+
}
452+
453+
@EnableWebSecurity
454+
@Import(Saml2LoginConfigBeans.class)
455+
static class CustomLoginProcessingUrlDefaultAuthenticationConverter {
456+
457+
@Bean
458+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
459+
// @formatter:off
460+
http
461+
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
462+
.saml2Login((saml2) -> saml2.loginProcessingUrl("/my/custom/url"));
463+
// @formatter:on
464+
return http.build();
465+
}
466+
467+
}
468+
469+
@EnableWebSecurity
470+
@Import(Saml2LoginConfigBeans.class)
471+
static class CustomLoginProcessingUrlCustomAuthenticationConverter {
472+
473+
@Bean
474+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
475+
// @formatter:off
476+
http
477+
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
478+
.saml2Login((saml2) -> saml2
479+
.loginProcessingUrl("/my/custom/url")
480+
.authenticationConverter(AUTHENTICATION_CONVERTER)
481+
);
482+
// @formatter:on
483+
return http.build();
484+
}
485+
486+
}
487+
398488
static class Saml2LoginConfigBeans {
399489

400490
@Bean

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,21 @@ public Saml2WebSsoAuthenticationFilter(RelyingPartyRegistrationRepository relyin
6363
String filterProcessesUrl) {
6464
this(new Saml2AuthenticationTokenConverter(
6565
new DefaultRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository)), filterProcessesUrl);
66+
Assert.isTrue(filterProcessesUrl.contains("{registrationId}"),
67+
"filterProcessesUrl must contain a {registrationId} match variable");
6668
}
6769

6870
/**
6971
* Creates a {@link Saml2WebSsoAuthenticationFilter} given the provided parameters
7072
* @param authenticationConverter the strategy for converting an
7173
* {@link HttpServletRequest} into an {@link Authentication}
72-
* @param filterProcessingUrl the processing URL, must contain a {registrationId}
73-
* variable
74+
* @param filterProcessesUrl the processing URL
7475
* @since 5.4
7576
*/
76-
public Saml2WebSsoAuthenticationFilter(AuthenticationConverter authenticationConverter,
77-
String filterProcessingUrl) {
78-
super(filterProcessingUrl);
77+
public Saml2WebSsoAuthenticationFilter(AuthenticationConverter authenticationConverter, String filterProcessesUrl) {
78+
super(filterProcessesUrl);
7979
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
80-
Assert.hasText(filterProcessingUrl, "filterProcessesUrl must contain a URL pattern");
81-
Assert.isTrue(filterProcessingUrl.contains("{registrationId}"),
82-
"filterProcessesUrl must contain a {registrationId} match variable");
80+
Assert.hasText(filterProcessesUrl, "filterProcessesUrl must contain a URL pattern");
8381
this.authenticationConverter = authenticationConverter;
8482
setAllowSessionCreation(true);
8583
setSessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy());

saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import org.springframework.mock.web.MockHttpServletResponse;
2727
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
2828
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
29+
import org.springframework.security.web.authentication.AuthenticationConverter;
2930

3031
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3132
import static org.mockito.BDDMockito.given;
@@ -60,6 +61,12 @@ public void constructingFilterWithValidRegistrationIdVariableThenSucceeds() {
6061
this.filter = new Saml2WebSsoAuthenticationFilter(this.repository, "/url/variable/is/present/{registrationId}");
6162
}
6263

64+
@Test
65+
public void constructingFilterWithMissingRegistrationIdVariableAndCustomAuthenticationConverterThenSucceeds() {
66+
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
67+
this.filter = new Saml2WebSsoAuthenticationFilter(authenticationConverter, "/url/missing/variable");
68+
}
69+
6370
@Test
6471
public void requiresAuthenticationWhenHappyPathThenReturnsTrue() {
6572
Assert.assertTrue(this.filter.requiresAuthentication(this.request, this.response));

0 commit comments

Comments
 (0)