Browse Source

Support custom validation in OidcLogoutAuthenticationProvider

- Similar to custom validation in OAuth2AuthorizationCodeRequestAuthenticationProvider

Closes gh-1693
pull/1725/head
Daniel Garnier-Moiroux 1 year ago committed by Joe Grandja
parent
commit
acd4fd0227
  1. 12
      docs/modules/ROOT/pages/protocol-endpoints.adoc
  2. 106
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationContext.java
  3. 35
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProvider.java
  4. 73
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationValidator.java
  5. 33
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProviderTests.java

12
docs/modules/ROOT/pages/protocol-endpoints.adoc

@ -554,6 +554,18 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h @@ -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

106
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationContext.java

@ -0,0 +1,106 @@ @@ -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());
}
}
}

35
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProvider.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 @@ -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 @@ -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 @@ -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;

73
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationValidator.java

@ -0,0 +1,73 @@ @@ -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);
}
}
}

33
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProviderTests.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 @@ -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 { @@ -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");

Loading…
Cancel
Save