Skip to content

Commit f45b990

Browse files
Allow SAML 2.0 loginProcessingURL without registrationId
Closes gh-10176
1 parent 4df9b45 commit f45b990

File tree

4 files changed

+103
-19
lines changed

4 files changed

+103
-19
lines changed

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,19 @@ public Saml2LoginConfigurer<B> loginPage(String loginPage) {
167167
return this;
168168
}
169169

170+
/**
171+
* Specifies the URL to validate the credentials. If specified a custom URL, consider
172+
* specifying a custom {@link AuthenticationConverter} via
173+
* {@link #authenticationConverter(AuthenticationConverter)}, since the default
174+
* {@link AuthenticationConverter} implementation relies on the
175+
* <code>{registrationId}</code> path variable to be present in the URL
176+
* @param loginProcessingUrl the URL to validate the credentials
177+
* @return the {@link Saml2LoginConfigurer} for additional customization
178+
* @see Saml2WebSsoAuthenticationFilter#DEFAULT_FILTER_PROCESSES_URI
179+
*/
170180
@Override
171181
public Saml2LoginConfigurer<B> loginProcessingUrl(String loginProcessingUrl) {
172182
Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty");
173-
Assert.state(loginProcessingUrl.contains("{registrationId}"), "{registrationId} path variable is required");
174183
this.loginProcessingUrl = loginProcessingUrl;
175184
return this;
176185
}
@@ -249,6 +258,8 @@ public void configure(B http) throws Exception {
249258

250259
private AuthenticationConverter getAuthenticationConverter(B http) {
251260
if (this.authenticationConverter == null) {
261+
Assert.state(this.loginProcessingUrl.contains("{registrationId}"),
262+
"loginProcessingUrl must contain {registrationId} path variable");
252263
return new Saml2AuthenticationTokenConverter(
253264
new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository));
254265
}

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

+77-9
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.opensaml.saml.saml2.core.Assertion;
4040
import org.opensaml.saml.saml2.core.AuthnRequest;
4141

42+
import org.springframework.beans.factory.BeanCreationException;
4243
import org.springframework.beans.factory.annotation.Autowired;
4344
import org.springframework.context.ConfigurableApplicationContext;
4445
import org.springframework.context.annotation.Bean;
@@ -62,6 +63,7 @@
6263
import org.springframework.security.core.authority.SimpleGrantedAuthority;
6364
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
6465
import org.springframework.security.saml2.Saml2Exception;
66+
import org.springframework.security.saml2.core.Saml2Utils;
6567
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
6668
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
6769
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
@@ -77,6 +79,7 @@
7779
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
7880
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver;
7981
import org.springframework.security.web.FilterChainProxy;
82+
import org.springframework.security.web.SecurityFilterChain;
8083
import org.springframework.security.web.authentication.AuthenticationConverter;
8184
import org.springframework.security.web.context.HttpRequestResponseHolder;
8285
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
@@ -89,6 +92,7 @@
8992
import org.springframework.web.util.UriComponentsBuilder;
9093

9194
import static org.assertj.core.api.Assertions.assertThat;
95+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
9296
import static org.mockito.ArgumentMatchers.any;
9397
import static org.mockito.ArgumentMatchers.anyString;
9498
import static org.mockito.BDDMockito.given;
@@ -115,6 +119,8 @@ public class Saml2LoginConfigurerTests {
115119

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

122+
private static final AuthenticationConverter AUTHENTICATION_CONVERTER = mock(AuthenticationConverter.class);
123+
118124
@Autowired
119125
private ConfigurableApplicationContext context;
120126

@@ -210,6 +216,33 @@ public void authenticateWhenCustomAuthenticationConverterThenUses() throws Excep
210216
verify(CustomAuthenticationConverter.authenticationConverter).convert(any(HttpServletRequest.class));
211217
}
212218

219+
@Test
220+
public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenValidates() {
221+
assertThatExceptionOfType(BeanCreationException.class)
222+
.isThrownBy(() -> this.spring.register(CustomLoginProcessingUrlDefaultAuthenticationConverter.class)
223+
.autowire())
224+
.havingRootCause().isInstanceOf(IllegalStateException.class)
225+
.withMessage("loginProcessingUrl must contain {registrationId} path variable");
226+
}
227+
228+
@Test
229+
public void authenticateWhenCustomLoginProcessingUrlAndCustomAuthenticationConverterThenAuthenticate()
230+
throws Exception {
231+
this.spring.register(CustomLoginProcessingUrlCustomAuthenticationConverter.class).autowire();
232+
RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.noCredentials()
233+
.assertingPartyDetails((party) -> party.verificationX509Credentials(
234+
(c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential())))
235+
.build();
236+
String response = new String(Saml2Utils.samlDecode(SIGNED_RESPONSE));
237+
given(AUTHENTICATION_CONVERTER.convert(any(HttpServletRequest.class)))
238+
.willReturn(new Saml2AuthenticationToken(relyingPartyRegistration, response));
239+
// @formatter:off
240+
MockHttpServletRequestBuilder request = post("/my/custom/url").param("SAMLResponse", SIGNED_RESPONSE);
241+
// @formatter:on
242+
this.mvc.perform(request).andExpect(redirectedUrl("/"));
243+
verify(AUTHENTICATION_CONVERTER).convert(any(HttpServletRequest.class));
244+
}
245+
213246
private void validateSaml2WebSsoAuthenticationFilterConfiguration() {
214247
// get the OpenSamlAuthenticationProvider
215248
Saml2WebSsoAuthenticationFilter filter = getSaml2SsoFilter(this.springSecurityFilterChain);
@@ -325,10 +358,10 @@ static class CustomAuthenticationRequestContextResolver extends WebSecurityConfi
325358
protected void configure(HttpSecurity http) throws Exception {
326359
// @formatter:off
327360
http
328-
.authorizeRequests((authz) -> authz
329-
.anyRequest().authenticated()
330-
)
331-
.saml2Login(withDefaults());
361+
.authorizeRequests((authz) -> authz
362+
.anyRequest().authenticated()
363+
)
364+
.saml2Login(withDefaults());
332365
// @formatter:on
333366
}
334367

@@ -347,11 +380,11 @@ static class CustomAuthenticationRequestContextConverterResolver extends WebSecu
347380
protected void configure(HttpSecurity http) throws Exception {
348381
// @formatter:off
349382
http
350-
.authorizeRequests((authz) -> authz
351-
.anyRequest().authenticated()
352-
)
353-
.saml2Login((saml2) -> {
354-
});
383+
.authorizeRequests((authz) -> authz
384+
.anyRequest().authenticated()
385+
)
386+
.saml2Login((saml2) -> {
387+
});
355388
// @formatter:on
356389
}
357390

@@ -382,6 +415,41 @@ protected void configure(HttpSecurity http) throws Exception {
382415

383416
}
384417

418+
@EnableWebSecurity
419+
@Import(Saml2LoginConfigBeans.class)
420+
static class CustomLoginProcessingUrlDefaultAuthenticationConverter {
421+
422+
@Bean
423+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
424+
// @formatter:off
425+
http
426+
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
427+
.saml2Login((saml2) -> saml2.loginProcessingUrl("/my/custom/url"));
428+
// @formatter:on
429+
return http.build();
430+
}
431+
432+
}
433+
434+
@EnableWebSecurity
435+
@Import(Saml2LoginConfigBeans.class)
436+
static class CustomLoginProcessingUrlCustomAuthenticationConverter {
437+
438+
@Bean
439+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
440+
// @formatter:off
441+
http
442+
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
443+
.saml2Login((saml2) -> saml2
444+
.loginProcessingUrl("/my/custom/url")
445+
.authenticationConverter(AUTHENTICATION_CONVERTER)
446+
);
447+
// @formatter:on
448+
return http.build();
449+
}
450+
451+
}
452+
385453
static class Saml2LoginConfigBeans {
386454

387455
@Bean

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

+6-8
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

+8-1
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)