Skip to content

Commit 6ead3f5

Browse files
committed
Support custom validation in OidcLogoutAuthenticationProvider
- Similar to custom validation in OAuth2AuthorizationCodeRequestAuthenticationProvider - Closes gh-1693
1 parent 052a0a6 commit 6ead3f5

File tree

4 files changed

+259
-7
lines changed

4 files changed

+259
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2020-2024 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+
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
17+
18+
import java.util.Map;
19+
import java.util.function.Consumer;
20+
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.security.core.Authentication;
23+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
24+
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationContext;
25+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* An {@link OAuth2AuthenticationContext} that holds an
30+
* {@link OidcLogoutAuthenticationToken} and additional information and is used when
31+
* validating the OpenID Connect RP-Initiated Logout Request parameters.
32+
*
33+
* @author Daniel Garnier-Moiroux
34+
* @since 1.4
35+
* @see OAuth2AuthenticationContext
36+
* @see OidcLogoutAuthenticationToken
37+
* @see OidcLogoutAuthenticationProvider#setAuthenticationValidator(Consumer)
38+
*/
39+
public final class OidcLogoutAuthenticationContext implements OAuth2AuthenticationContext {
40+
41+
private final Map<Object, Object> context;
42+
43+
private OidcLogoutAuthenticationContext(Map<Object, Object> context) {
44+
this.context = context;
45+
}
46+
47+
@SuppressWarnings("unchecked")
48+
@Nullable
49+
@Override
50+
public <V> V get(Object key) {
51+
return hasKey(key) ? (V) this.context.get(key) : null;
52+
}
53+
54+
@Override
55+
public boolean hasKey(Object key) {
56+
Assert.notNull(key, "key cannot be null");
57+
return this.context.containsKey(key);
58+
}
59+
60+
/**
61+
* Returns the {@link RegisteredClient registered client}.
62+
* @return the {@link RegisteredClient}
63+
*/
64+
public RegisteredClient getRegisteredClient() {
65+
return get(RegisteredClient.class);
66+
}
67+
68+
/**
69+
* Returns the {@link OidcIdToken id_token}.
70+
* @return the {@link OidcIdToken}
71+
*/
72+
public OidcIdToken getIdToken() {
73+
return get(OidcIdToken.class);
74+
}
75+
76+
/**
77+
* Constructs a new {@link Builder} with the provided
78+
* {@link OidcLogoutAuthenticationToken}.
79+
* @param authentication the {@link OidcLogoutAuthenticationToken}
80+
* @return the {@link Builder}
81+
*/
82+
public static Builder with(OidcLogoutAuthenticationToken authentication) {
83+
return new Builder(authentication);
84+
}
85+
86+
/**
87+
* A builder for {@link OidcLogoutAuthenticationContext}.
88+
*/
89+
public static final class Builder extends AbstractBuilder<OidcLogoutAuthenticationContext, Builder> {
90+
91+
private Builder(Authentication authentication) {
92+
super(authentication);
93+
}
94+
95+
/**
96+
* Sets the {@link RegisteredClient registered client}.
97+
* @param registeredClient the {@link RegisteredClient}
98+
* @return the {@link Builder} for further configuration
99+
*/
100+
public Builder registeredClient(RegisteredClient registeredClient) {
101+
return put(RegisteredClient.class, registeredClient);
102+
}
103+
104+
/**
105+
* Sets the {@link OidcIdToken id_token}.
106+
* @param idToken the {@link OidcIdToken}
107+
* @return the {@link Builder} for further configuration
108+
*/
109+
public Builder idToken(OidcIdToken idToken) {
110+
return put(OidcIdToken.class, idToken);
111+
}
112+
113+
/**
114+
* Builds a new {@link OidcLogoutAuthenticationContext}.
115+
* @return the {@link OidcLogoutAuthenticationContext}
116+
*/
117+
@Override
118+
public OidcLogoutAuthenticationContext build() {
119+
return new OidcLogoutAuthenticationContext(getContext());
120+
}
121+
122+
}
123+
124+
}

Diff for: oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProvider.java

+30-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2024 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.
@@ -21,6 +21,7 @@
2121
import java.security.Principal;
2222
import java.util.Base64;
2323
import java.util.List;
24+
import java.util.function.Consumer;
2425

2526
import org.apache.commons.logging.Log;
2627
import org.apache.commons.logging.LogFactory;
@@ -70,6 +71,8 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro
7071

7172
private final SessionRegistry sessionRegistry;
7273

74+
private Consumer<OidcLogoutAuthenticationContext> authenticationValidator = new OidcLogoutAuthenticationValidator();
75+
7376
/**
7477
* Constructs an {@code OidcLogoutAuthenticationProvider} using the provided
7578
* parameters.
@@ -126,11 +129,12 @@ public Authentication authenticate(Authentication authentication) throws Authent
126129
&& !oidcLogoutAuthentication.getClientId().equals(registeredClient.getClientId())) {
127130
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
128131
}
129-
if (StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())
130-
&& !registeredClient.getPostLogoutRedirectUris()
131-
.contains(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
132-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "post_logout_redirect_uri");
133-
}
132+
133+
OidcLogoutAuthenticationContext context = OidcLogoutAuthenticationContext.with(oidcLogoutAuthentication)
134+
.registeredClient(registeredClient)
135+
.idToken(idToken)
136+
.build();
137+
this.authenticationValidator.accept(context);
134138

135139
if (this.logger.isTraceEnabled()) {
136140
this.logger.trace("Validated logout request parameters");
@@ -182,6 +186,26 @@ public boolean supports(Class<?> authentication) {
182186
return OidcLogoutAuthenticationToken.class.isAssignableFrom(authentication);
183187
}
184188

189+
/**
190+
* Sets the {@code Consumer} providing access to the
191+
* {@link OidcLogoutAuthenticationContext} and is responsible for validating specific
192+
* Open ID Connect RP-Initiated Logout Request parameters associated in the
193+
* {@link OidcLogoutAuthenticationToken}. The default authentication validator is
194+
* {@link OidcLogoutAuthenticationValidator}.
195+
*
196+
* <p>
197+
* <b>NOTE:</b> The authentication validator MUST throw
198+
* {@link OAuth2AuthenticationException} if validation fails.
199+
* @param authenticationValidator the {@code Consumer} providing access to the
200+
* {@link OidcLogoutAuthenticationContext} and is responsible for validating specific
201+
* Open ID Connect RP-Initiated Logout Request parameters
202+
* @since 1.4
203+
*/
204+
public void setAuthenticationValidator(Consumer<OidcLogoutAuthenticationContext> authenticationValidator) {
205+
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
206+
this.authenticationValidator = authenticationValidator;
207+
}
208+
185209
private SessionInformation findSessionInformation(Authentication principal, String sessionId) {
186210
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(principal.getPrincipal(), true);
187211
SessionInformation sessionInformation = null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2020-2024 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+
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
17+
18+
import java.util.function.Consumer;
19+
20+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
21+
import org.springframework.security.oauth2.core.OAuth2Error;
22+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
23+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
24+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
25+
import org.springframework.util.StringUtils;
26+
27+
/**
28+
* A {@code Consumer} providing access to the {@link OidcLogoutAuthenticationContext}
29+
* containing an {@link OidcLogoutAuthenticationToken} and is the default
30+
* {@link OidcLogoutAuthenticationProvider#setAuthenticationValidator(Consumer)
31+
* authentication validator} used for validating specific OpenID Connect RP-Initiated
32+
* Logout parameters used in the Authorization Code Grant.
33+
*
34+
* <p>
35+
* The default implementation first validates {@link OidcIdToken#getAudience()}, and then
36+
* {@link OidcLogoutAuthenticationToken#getPostLogoutRedirectUri()}. If validation fails,
37+
* an {@link OAuth2AuthenticationException} is thrown.
38+
*
39+
* @author Daniel Garnier-Moiroux
40+
* @since 1.4
41+
* @see OidcLogoutAuthenticationContext
42+
* @see OidcLogoutAuthenticationToken
43+
* @see OidcLogoutAuthenticationProvider#setAuthenticationValidator(Consumer)
44+
*/
45+
public final class OidcLogoutAuthenticationValidator implements Consumer<OidcLogoutAuthenticationContext> {
46+
47+
/**
48+
* The default validator for
49+
* {@link OidcLogoutAuthenticationToken#getPostLogoutRedirectUri()}.
50+
*/
51+
public static final Consumer<OidcLogoutAuthenticationContext> DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcLogoutAuthenticationValidator::validatePostLogoutRedirectUri;
52+
53+
private final Consumer<OidcLogoutAuthenticationContext> authenticationValidator = DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR;
54+
55+
@Override
56+
public void accept(OidcLogoutAuthenticationContext authenticationContext) {
57+
this.authenticationValidator.accept(authenticationContext);
58+
}
59+
60+
private static void validatePostLogoutRedirectUri(OidcLogoutAuthenticationContext authenticationContext) {
61+
OidcLogoutAuthenticationToken oidcLogoutAuthentication = authenticationContext.getAuthentication();
62+
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
63+
if (StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())
64+
&& !registeredClient.getPostLogoutRedirectUris()
65+
.contains(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
66+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
67+
"OpenID Connect 1.0 Logout Request Parameter: post_logout_redirect_uri",
68+
"https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ValidationAndErrorHandling");
69+
throw new OAuth2AuthenticationException(error);
70+
}
71+
}
72+
73+
}

Diff for: oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProviderTests.java

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2024 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.
@@ -24,6 +24,7 @@
2424
import java.util.Collections;
2525
import java.util.Date;
2626
import java.util.List;
27+
import java.util.function.Consumer;
2728

2829
import org.junit.jupiter.api.AfterEach;
2930
import org.junit.jupiter.api.BeforeEach;
@@ -53,6 +54,7 @@
5354
import static org.assertj.core.api.Assertions.assertThat;
5455
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
5556
import static org.assertj.core.api.Assertions.assertThatThrownBy;
57+
import static org.mockito.ArgumentMatchers.any;
5658
import static org.mockito.ArgumentMatchers.eq;
5759
import static org.mockito.BDDMockito.given;
5860
import static org.mockito.Mockito.mock;
@@ -314,6 +316,35 @@ public void authenticateWhenInvalidPostLogoutRedirectUriThenThrowOAuth2Authentic
314316
verify(this.registeredClientRepository).findById(eq(authorization.getRegisteredClientId()));
315317
}
316318

319+
@Test
320+
void setAuthenticationValidatorWhenNullThenThrowIllegalArgumentException() {
321+
assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidator(null))
322+
.isInstanceOf(IllegalArgumentException.class)
323+
.hasMessage("authenticationValidator cannot be null");
324+
}
325+
326+
@Test
327+
public void authenticateWhenCustomAuthenticationValidatorThenUsed() throws NoSuchAlgorithmException {
328+
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
329+
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
330+
String sessionId = "session-1";
331+
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
332+
.issuer("https://provider.com")
333+
.subject(principal.getName())
334+
.audience(Collections.singleton(registeredClient.getClientId()))
335+
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
336+
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
337+
.claim("sid", createHash(sessionId))
338+
.build();
339+
340+
@SuppressWarnings("unchecked")
341+
Consumer<OidcLogoutAuthenticationContext> authenticationValidator = mock(Consumer.class);
342+
this.authenticationProvider.setAuthenticationValidator(authenticationValidator);
343+
344+
authenticateValidIdToken(principal, registeredClient, sessionId, idToken);
345+
verify(authenticationValidator).accept(any());
346+
}
347+
317348
@Test
318349
public void authenticateWhenMissingSubThenThrowOAuth2AuthenticationException() {
319350
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");

0 commit comments

Comments
 (0)