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]
|
[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].
|
`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]]
|
[[oidc-user-info-endpoint]]
|
||||||
== OpenID Connect 1.0 UserInfo 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.security.Principal;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
@@ -70,6 +71,8 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro
|
|||||||
|
|
||||||
private final SessionRegistry sessionRegistry;
|
private final SessionRegistry sessionRegistry;
|
||||||
|
|
||||||
|
private Consumer<OidcLogoutAuthenticationContext> authenticationValidator = new OidcLogoutAuthenticationValidator();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an {@code OidcLogoutAuthenticationProvider} using the provided
|
* Constructs an {@code OidcLogoutAuthenticationProvider} using the provided
|
||||||
* parameters.
|
* parameters.
|
||||||
@@ -126,11 +129,11 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro
|
|||||||
&& !oidcLogoutAuthentication.getClientId().equals(registeredClient.getClientId())) {
|
&& !oidcLogoutAuthentication.getClientId().equals(registeredClient.getClientId())) {
|
||||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())
|
|
||||||
&& !registeredClient.getPostLogoutRedirectUris()
|
OidcLogoutAuthenticationContext context = OidcLogoutAuthenticationContext.with(oidcLogoutAuthentication)
|
||||||
.contains(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
|
.registeredClient(registeredClient)
|
||||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "post_logout_redirect_uri");
|
.build();
|
||||||
}
|
this.authenticationValidator.accept(context);
|
||||||
|
|
||||||
if (this.logger.isTraceEnabled()) {
|
if (this.logger.isTraceEnabled()) {
|
||||||
this.logger.trace("Validated logout request parameters");
|
this.logger.trace("Validated logout request parameters");
|
||||||
@@ -182,6 +185,26 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro
|
|||||||
return OidcLogoutAuthenticationToken.class.isAssignableFrom(authentication);
|
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) {
|
private SessionInformation findSessionInformation(Authentication principal, String sessionId) {
|
||||||
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(principal.getPrincipal(), true);
|
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(principal.getPrincipal(), true);
|
||||||
SessionInformation sessionInformation = null;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
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.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -314,6 +316,35 @@ public class OidcLogoutAuthenticationProviderTests {
|
|||||||
verify(this.registeredClientRepository).findById(eq(authorization.getRegisteredClientId()));
|
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
|
@Test
|
||||||
public void authenticateWhenMissingSubThenThrowOAuth2AuthenticationException() {
|
public void authenticateWhenMissingSubThenThrowOAuth2AuthenticationException() {
|
||||||
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
|
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
|
||||||
|
|||||||
Reference in New Issue
Block a user