Support custom validation in OidcLogoutAuthenticationProvider
- Similar to custom validation in OAuth2AuthorizationCodeRequestAuthenticationProvider Closes gh-1693
This commit is contained in:
committed by
Joe Grandja
parent
7f82a0b9f4
commit
acd4fd0227
@@ -554,6 +554,18 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
|
||||
[TIP]
|
||||
`OidcClientInitiatedLogoutSuccessHandler` is the corresponding configuration in Spring Security’s OAuth2 Client support for configuring {spring-security-reference-base-url}/servlet/oauth2/login/advanced.html#oauth2login-advanced-oidc-logout[OpenID Connect 1.0 RP-Initiated Logout].
|
||||
|
||||
[[oidc-logout-endpoint-customizing-logout-request-validation]]
|
||||
=== Customizing Logout Request Validation
|
||||
|
||||
`OidcLogoutAuthenticationValidator` is the default validator used for validating specific OpenID Connect Logout request parameters used in the RP-Initiated Logout flow.
|
||||
The default implementation validates the `post_logout_redirect_uri` parameter.
|
||||
If validation fails, an `OAuth2AuthenticationException` is thrown.
|
||||
|
||||
`OidcLogoutAuthenticationProvider` provides the ability to override the default logout request validation by supplying a custom authentication validator of type `Consumer<OidcLogoutAuthenticationContext>` to `setAuthenticationValidator()`.
|
||||
|
||||
[IMPORTANT]
|
||||
If validation fails, the authentication validator *MUST* throw `OAuth2AuthenticationException`.
|
||||
|
||||
[[oidc-user-info-endpoint]]
|
||||
== OpenID Connect 1.0 UserInfo Endpoint
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationContext;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthenticationContext} that holds an
|
||||
* {@link OidcLogoutAuthenticationToken} and additional information and is used when
|
||||
* validating the OpenID Connect RP-Initiated Logout Request parameters.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 1.4
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OidcLogoutAuthenticationToken
|
||||
* @see OidcLogoutAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OidcLogoutAuthenticationContext implements OAuth2AuthenticationContext {
|
||||
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
private OidcLogoutAuthenticationContext(Map<Object, Object> context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link RegisteredClient registered client}.
|
||||
* @return the {@link RegisteredClient}
|
||||
*/
|
||||
public RegisteredClient getRegisteredClient() {
|
||||
return get(RegisteredClient.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided
|
||||
* {@link OidcLogoutAuthenticationToken}.
|
||||
* @param authentication the {@link OidcLogoutAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OidcLogoutAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OidcLogoutAuthenticationContext}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OidcLogoutAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(Authentication authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link RegisteredClient registered client}.
|
||||
* @param registeredClient the {@link RegisteredClient}
|
||||
* @return the {@link Builder} for further configuration
|
||||
*/
|
||||
public Builder registeredClient(RegisteredClient registeredClient) {
|
||||
return put(RegisteredClient.class, registeredClient);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OidcLogoutAuthenticationContext}.
|
||||
* @return the {@link OidcLogoutAuthenticationContext}
|
||||
*/
|
||||
@Override
|
||||
public OidcLogoutAuthenticationContext build() {
|
||||
return new OidcLogoutAuthenticationContext(getContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,6 +21,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Principal;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -70,6 +71,8 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro
|
||||
|
||||
private final SessionRegistry sessionRegistry;
|
||||
|
||||
private Consumer<OidcLogoutAuthenticationContext> authenticationValidator = new OidcLogoutAuthenticationValidator();
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcLogoutAuthenticationProvider} using the provided
|
||||
* parameters.
|
||||
@@ -126,11 +129,11 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro
|
||||
&& !oidcLogoutAuthentication.getClientId().equals(registeredClient.getClientId())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
||||
}
|
||||
if (StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())
|
||||
&& !registeredClient.getPostLogoutRedirectUris()
|
||||
.contains(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "post_logout_redirect_uri");
|
||||
}
|
||||
|
||||
OidcLogoutAuthenticationContext context = OidcLogoutAuthenticationContext.with(oidcLogoutAuthentication)
|
||||
.registeredClient(registeredClient)
|
||||
.build();
|
||||
this.authenticationValidator.accept(context);
|
||||
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Validated logout request parameters");
|
||||
@@ -182,6 +185,26 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro
|
||||
return OidcLogoutAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the
|
||||
* {@link OidcLogoutAuthenticationContext} and is responsible for validating specific
|
||||
* Open ID Connect RP-Initiated Logout Request parameters associated in the
|
||||
* {@link OidcLogoutAuthenticationToken}. The default authentication validator is
|
||||
* {@link OidcLogoutAuthenticationValidator}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The authentication validator MUST throw
|
||||
* {@link OAuth2AuthenticationException} if validation fails.
|
||||
* @param authenticationValidator the {@code Consumer} providing access to the
|
||||
* {@link OidcLogoutAuthenticationContext} and is responsible for validating specific
|
||||
* Open ID Connect RP-Initiated Logout Request parameters
|
||||
* @since 1.4
|
||||
*/
|
||||
public void setAuthenticationValidator(Consumer<OidcLogoutAuthenticationContext> authenticationValidator) {
|
||||
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||
this.authenticationValidator = authenticationValidator;
|
||||
}
|
||||
|
||||
private SessionInformation findSessionInformation(Authentication principal, String sessionId) {
|
||||
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(principal.getPrincipal(), true);
|
||||
SessionInformation sessionInformation = null;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@code Consumer} providing access to the {@link OidcLogoutAuthenticationContext}
|
||||
* containing an {@link OidcLogoutAuthenticationToken} and is the default
|
||||
* {@link OidcLogoutAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
* authentication validator} used for validating specific OpenID Connect RP-Initiated
|
||||
* Logout parameters used in the Authorization Code Grant.
|
||||
*
|
||||
* <p>
|
||||
* The default implementation first validates {@link OidcIdToken#getAudience()}, and then
|
||||
* {@link OidcLogoutAuthenticationToken#getPostLogoutRedirectUri()}. If validation fails,
|
||||
* an {@link OAuth2AuthenticationException} is thrown.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
* @since 1.4
|
||||
* @see OidcLogoutAuthenticationContext
|
||||
* @see OidcLogoutAuthenticationToken
|
||||
* @see OidcLogoutAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OidcLogoutAuthenticationValidator implements Consumer<OidcLogoutAuthenticationContext> {
|
||||
|
||||
/**
|
||||
* The default validator for
|
||||
* {@link OidcLogoutAuthenticationToken#getPostLogoutRedirectUri()}.
|
||||
*/
|
||||
public static final Consumer<OidcLogoutAuthenticationContext> DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcLogoutAuthenticationValidator::validatePostLogoutRedirectUri;
|
||||
|
||||
private final Consumer<OidcLogoutAuthenticationContext> authenticationValidator = DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR;
|
||||
|
||||
@Override
|
||||
public void accept(OidcLogoutAuthenticationContext authenticationContext) {
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
}
|
||||
|
||||
private static void validatePostLogoutRedirectUri(OidcLogoutAuthenticationContext authenticationContext) {
|
||||
OidcLogoutAuthenticationToken oidcLogoutAuthentication = authenticationContext.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
|
||||
if (StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())
|
||||
&& !registeredClient.getPostLogoutRedirectUris()
|
||||
.contains(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
|
||||
"OpenID Connect 1.0 Logout Request Parameter: post_logout_redirect_uri",
|
||||
"https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ValidationAndErrorHandling");
|
||||
throw new OAuth2AuthenticationException(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,6 +24,7 @@ import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -53,6 +54,7 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -314,6 +316,35 @@ public class OidcLogoutAuthenticationProviderTests {
|
||||
verify(this.registeredClientRepository).findById(eq(authorization.getRegisteredClientId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setAuthenticationValidatorWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidator(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authenticationValidator cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenCustomAuthenticationValidatorThenUsed() throws NoSuchAlgorithmException {
|
||||
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
String sessionId = "session-1";
|
||||
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
|
||||
.issuer("https://provider.com")
|
||||
.subject(principal.getName())
|
||||
.audience(Collections.singleton(registeredClient.getClientId()))
|
||||
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
|
||||
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
|
||||
.claim("sid", createHash(sessionId))
|
||||
.build();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Consumer<OidcLogoutAuthenticationContext> authenticationValidator = mock(Consumer.class);
|
||||
this.authenticationProvider.setAuthenticationValidator(authenticationValidator);
|
||||
|
||||
authenticateValidIdToken(principal, registeredClient, sessionId, idToken);
|
||||
verify(authenticationValidator).accept(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenMissingSubThenThrowOAuth2AuthenticationException() {
|
||||
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
|
||||
|
||||
Reference in New Issue
Block a user