Skip to content

Commit f802440

Browse files
marcusdacoregioAyush Kohli
authored andcommitted
Add Saml2AuthenticationRequestRepository
Closes spring-projectsgh-9185
1 parent 9d55aa2 commit f802440

File tree

15 files changed

+655
-17
lines changed

15 files changed

+655
-17
lines changed

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

Lines changed: 26 additions & 3 deletions
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.
@@ -33,13 +33,16 @@
3333
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
3434
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
3535
import org.springframework.security.core.Authentication;
36+
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
3637
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
3738
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory;
3839
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
3940
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory;
4041
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory;
4142
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
4243
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
44+
import org.springframework.security.saml2.provider.service.servlet.HttpSessionSaml2AuthenticationRequestRepository;
45+
import org.springframework.security.saml2.provider.service.servlet.Saml2AuthenticationRequestRepository;
4346
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
4447
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter;
4548
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
@@ -206,6 +209,7 @@ public void init(B http) throws Exception {
206209
}
207210
this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http),
208211
this.loginProcessingUrl);
212+
setAuthenticationRequestRepository(http, this.saml2WebSsoAuthenticationFilter);
209213
setAuthenticationFilter(this.saml2WebSsoAuthenticationFilter);
210214
super.loginProcessingUrl(this.loginProcessingUrl);
211215
if (StringUtils.hasText(this.loginPage)) {
@@ -252,6 +256,11 @@ public void configure(B http) throws Exception {
252256
}
253257
}
254258

259+
private void setAuthenticationRequestRepository(B http,
260+
Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter) {
261+
saml2WebSsoAuthenticationFilter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));
262+
}
263+
255264
private AuthenticationConverter getAuthenticationConverter(B http) {
256265
if (this.authenticationConverter == null) {
257266
return new Saml2AuthenticationTokenConverter(
@@ -311,6 +320,16 @@ private Map<String, String> getIdentityProviderUrlMap(String authRequestPrefixUr
311320
return idps;
312321
}
313322

323+
private Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> getAuthenticationRequestRepository(
324+
B http) {
325+
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> repository = getBeanOrNull(http,
326+
Saml2AuthenticationRequestRepository.class);
327+
if (repository == null) {
328+
return new HttpSessionSaml2AuthenticationRequestRepository();
329+
}
330+
return repository;
331+
}
332+
314333
private <C> C getSharedOrBean(B http, Class<C> clazz) {
315334
C shared = http.getSharedObject(clazz);
316335
if (shared != null) {
@@ -348,8 +367,12 @@ private AuthenticationRequestEndpointConfig() {
348367
private Filter build(B http) {
349368
Saml2AuthenticationRequestFactory authenticationRequestResolver = getResolver(http);
350369
Saml2AuthenticationRequestContextResolver contextResolver = getContextResolver(http);
351-
return postProcess(
352-
new Saml2WebSsoAuthenticationRequestFilter(contextResolver, authenticationRequestResolver));
370+
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> repository = getAuthenticationRequestRepository(
371+
http);
372+
Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(contextResolver,
373+
authenticationRequestResolver);
374+
filter.setAuthenticationRequestRepository(repository);
375+
return postProcess(filter);
353376
}
354377

355378
private Saml2AuthenticationRequestFactory getResolver(B http) {

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

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import org.springframework.security.saml2.core.Saml2ErrorCodes;
6464
import org.springframework.security.saml2.core.Saml2Utils;
6565
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
66+
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
6667
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
6768
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationRequestFactory;
6869
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
@@ -76,9 +77,11 @@
7677
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
7778
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
7879
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
80+
import org.springframework.security.saml2.provider.service.servlet.Saml2AuthenticationRequestRepository;
7981
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
8082
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver;
8183
import org.springframework.security.web.FilterChainProxy;
84+
import org.springframework.security.web.SecurityFilterChain;
8285
import org.springframework.security.web.authentication.AuthenticationConverter;
8386
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
8487
import org.springframework.security.web.context.HttpRequestResponseHolder;
@@ -237,6 +240,29 @@ public void authenticateWithInvalidDeflatedSAMLResponseThenFailureHandlerUses()
237240
assertThat(exception.getCause()).isInstanceOf(IOException.class);
238241
}
239242

243+
@Test
244+
public void authenticationRequestWhenCustomAuthnRequestRepositoryThenUses() throws Exception {
245+
this.spring.register(CustomAuthenticationRequestRepository.class).autowire();
246+
MockHttpServletRequestBuilder request = get("/saml2/authenticate/registration-id");
247+
this.mvc.perform(request).andExpect(status().isFound());
248+
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> repository = this.spring.getContext()
249+
.getBean(Saml2AuthenticationRequestRepository.class);
250+
verify(repository).saveAuthenticationRequest(any(AbstractSaml2AuthenticationRequest.class),
251+
any(HttpServletRequest.class), any(HttpServletResponse.class));
252+
}
253+
254+
@Test
255+
public void authenticateWhenCustomAuthnRequestRepositoryThenUses() throws Exception {
256+
this.spring.register(CustomAuthenticationRequestRepository.class).autowire();
257+
MockHttpServletRequestBuilder request = post("/login/saml2/sso/registration-id").param("SAMLResponse",
258+
SIGNED_RESPONSE);
259+
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> repository = this.spring.getContext()
260+
.getBean(Saml2AuthenticationRequestRepository.class);
261+
this.mvc.perform(request);
262+
verify(repository).loadAuthenticationRequest(any(HttpServletRequest.class));
263+
verify(repository).removeAuthenticationRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
264+
}
265+
240266
private void validateSaml2WebSsoAuthenticationFilterConfiguration() {
241267
// get the OpenSamlAuthenticationProvider
242268
Saml2WebSsoAuthenticationFilter filter = getSaml2SsoFilter(this.springSecurityFilterChain);
@@ -371,7 +397,7 @@ protected void configure(HttpSecurity http) throws Exception {
371397

372398
@Bean
373399
Saml2AuthenticationRequestContextResolver resolver() {
374-
return resolver;
400+
return this.resolver;
375401
}
376402

377403
}
@@ -420,6 +446,27 @@ protected void configure(HttpSecurity http) throws Exception {
420446

421447
}
422448

449+
@EnableWebSecurity
450+
@Import(Saml2LoginConfigBeans.class)
451+
static class CustomAuthenticationRequestRepository {
452+
453+
private static final Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> repository = mock(
454+
Saml2AuthenticationRequestRepository.class);
455+
456+
@Bean
457+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
458+
http.authorizeRequests((authz) -> authz.anyRequest().authenticated());
459+
http.saml2Login(withDefaults());
460+
return http.build();
461+
}
462+
463+
@Bean
464+
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() {
465+
return this.repository;
466+
}
467+
468+
}
469+
423470
static class Saml2LoginConfigBeans {
424471

425472
@Bean

docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,3 +1610,33 @@ http {
16101610
The success handler will send logout requests to the asserting party.
16111611

16121612
The request matcher will detect logout requests from the asserting party.
1613+
1614+
[[servlet-saml2login-store-authn-request]]
1615+
=== Storing the `AuthnRequest`
1616+
1617+
The `Saml2AuthenticationRequestRepository` is responsible for the persistence of the `AuthnRequest` from the time the `AuthnRequest` <<servlet-saml2login-sp-initiated-factory,is initiated>> to the time the `SAMLResponse` <<servlet-saml2login-authenticate-responses,is received>>.
1618+
The `Saml2AuthenticationTokenConverter` is responsible for loading the `AuthnRequest` from the `Saml2AuthenticationRequestRepository` and saving it into the `Saml2AuthenticationToken`.
1619+
1620+
The default implementation of `Saml2AuthenticationRequestRepository` is `HttpSessionSaml2AuthenticationRequestRepository`, which stores the `AuthnRequest` in the `HttpSession`.
1621+
1622+
If you have a custom implementation of `Saml2AuthenticationRequestRepository`, you may configure it by exposing it as a `@Bean` as shown in the following example:
1623+
1624+
====
1625+
.Java
1626+
[source,java,role="primary"]
1627+
----
1628+
@Bean
1629+
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() {
1630+
return new CustomSaml2AuthenticationRequestRepository();
1631+
}
1632+
----
1633+
1634+
.Kotlin
1635+
[source,kotlin,role="secondary"]
1636+
----
1637+
@Bean
1638+
open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
1639+
return CustomSaml2AuthenticationRequestRepository()
1640+
}
1641+
----
1642+
====

saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ dependencies {
5555
testImplementation "org.junit.jupiter:junit-jupiter-params"
5656
testImplementation "org.junit.jupiter:junit-jupiter-engine"
5757
testImplementation "org.mockito:mockito-core"
58+
testImplementation "org.mockito:mockito-inline"
5859
testImplementation "org.mockito:mockito-junit-jupiter"
5960
testImplementation "org.springframework:spring-test"
6061
}

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationToken.java

Lines changed: 37 additions & 4 deletions
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.
@@ -38,8 +38,10 @@ public class Saml2AuthenticationToken extends AbstractAuthenticationToken {
3838

3939
private final String saml2Response;
4040

41+
private final AbstractSaml2AuthenticationRequest authenticationRequest;
42+
4143
/**
42-
* Creates a {@link Saml2AuthenticationToken} with the provided parameters
44+
* Creates a {@link Saml2AuthenticationToken} with the provided parameters.
4345
*
4446
* Note that the given {@link RelyingPartyRegistration} should have all its templates
4547
* resolved at this point. See
@@ -48,15 +50,35 @@ public class Saml2AuthenticationToken extends AbstractAuthenticationToken {
4850
* @param relyingPartyRegistration the resolved {@link RelyingPartyRegistration} to
4951
* use
5052
* @param saml2Response the SAML 2.0 response to authenticate
53+
* @param authenticationRequest the {@code AuthNRequest} sent to the asserting party
5154
*
52-
* @since 5.4
55+
* @since 5.6
5356
*/
54-
public Saml2AuthenticationToken(RelyingPartyRegistration relyingPartyRegistration, String saml2Response) {
57+
public Saml2AuthenticationToken(RelyingPartyRegistration relyingPartyRegistration, String saml2Response,
58+
AbstractSaml2AuthenticationRequest authenticationRequest) {
5559
super(Collections.emptyList());
5660
Assert.notNull(relyingPartyRegistration, "relyingPartyRegistration cannot be null");
5761
Assert.notNull(saml2Response, "saml2Response cannot be null");
5862
this.relyingPartyRegistration = relyingPartyRegistration;
5963
this.saml2Response = saml2Response;
64+
this.authenticationRequest = authenticationRequest;
65+
}
66+
67+
/**
68+
* Creates a {@link Saml2AuthenticationToken} with the provided parameters
69+
*
70+
* Note that the given {@link RelyingPartyRegistration} should have all its templates
71+
* resolved at this point. See
72+
* {@link org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter}
73+
* for an example of performing that resolution.
74+
* @param relyingPartyRegistration the resolved {@link RelyingPartyRegistration} to
75+
* use
76+
* @param saml2Response the SAML 2.0 response to authenticate
77+
*
78+
* @since 5.4
79+
*/
80+
public Saml2AuthenticationToken(RelyingPartyRegistration relyingPartyRegistration, String saml2Response) {
81+
this(relyingPartyRegistration, saml2Response, null);
6082
}
6183

6284
/**
@@ -81,6 +103,7 @@ public Saml2AuthenticationToken(String saml2Response, String recipientUri, Strin
81103
.entityId(idpEntityId).singleSignOnServiceLocation(idpEntityId))
82104
.build();
83105
this.saml2Response = saml2Response;
106+
this.authenticationRequest = null;
84107
}
85108

86109
/**
@@ -179,4 +202,14 @@ public String getIdpEntityId() {
179202
return this.relyingPartyRegistration.getAssertingPartyDetails().getEntityId();
180203
}
181204

205+
/**
206+
* Returns the authentication request sent to the assertion party or {@code null} if
207+
* no authentication request is present
208+
* @return the authentication request sent to the assertion party
209+
* @since 5.6
210+
*/
211+
public AbstractSaml2AuthenticationRequest getAuthenticationRequest() {
212+
return this.authenticationRequest;
213+
}
214+
182215
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.saml2.provider.service.servlet;
18+
19+
import javax.servlet.http.HttpServletRequest;
20+
import javax.servlet.http.HttpServletResponse;
21+
import javax.servlet.http.HttpSession;
22+
23+
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
24+
25+
/**
26+
* A {@link Saml2AuthenticationRequestRepository} implementation that uses
27+
* {@link HttpSession} to store and retrieve the
28+
* {@link AbstractSaml2AuthenticationRequest}
29+
*
30+
* @author Marcus Da Coregio
31+
* @since 5.6
32+
*/
33+
public class HttpSessionSaml2AuthenticationRequestRepository
34+
implements Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
35+
36+
private static final String DEFAULT_SAML2_AUTHN_REQUEST_ATTR_NAME = HttpSessionSaml2AuthenticationRequestRepository.class
37+
.getName().concat(".SAML2_AUTHN_REQUEST");
38+
39+
private String saml2AuthnRequestAttributeName = DEFAULT_SAML2_AUTHN_REQUEST_ATTR_NAME;
40+
41+
@Override
42+
public AbstractSaml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
43+
HttpSession httpSession = request.getSession(false);
44+
if (httpSession == null) {
45+
return null;
46+
}
47+
return (AbstractSaml2AuthenticationRequest) httpSession.getAttribute(this.saml2AuthnRequestAttributeName);
48+
}
49+
50+
@Override
51+
public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
52+
HttpServletRequest request, HttpServletResponse response) {
53+
if (authenticationRequest == null) {
54+
removeAuthenticationRequest(request, response);
55+
return;
56+
}
57+
HttpSession httpSession = request.getSession();
58+
httpSession.setAttribute(this.saml2AuthnRequestAttributeName, authenticationRequest);
59+
}
60+
61+
@Override
62+
public AbstractSaml2AuthenticationRequest removeAuthenticationRequest(HttpServletRequest request,
63+
HttpServletResponse response) {
64+
AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request);
65+
if (authenticationRequest == null) {
66+
return null;
67+
}
68+
HttpSession httpSession = request.getSession();
69+
httpSession.removeAttribute(this.saml2AuthnRequestAttributeName);
70+
return authenticationRequest;
71+
}
72+
73+
}

0 commit comments

Comments
 (0)