Add OpenID Connect 1.0 Logout Endpoint

Closes gh-266
This commit is contained in:
Joe Grandja
2023-01-30 10:47:36 -05:00
parent 7c6516bbbb
commit 98e3fe807a
66 changed files with 2718 additions and 84 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -39,6 +39,8 @@ public class Client {
@Column(length = 1000)
private String redirectUris;
@Column(length = 1000)
private String postLogoutRedirectUris;
@Column(length = 1000)
private String scopes;
@Column(length = 2000)
private String clientSettings;
@@ -118,6 +120,14 @@ public class Client {
this.redirectUris = redirectUris;
}
public String getPostLogoutRedirectUris() {
return this.postLogoutRedirectUris;
}
public void setPostLogoutRedirectUris(String postLogoutRedirectUris) {
this.postLogoutRedirectUris = postLogoutRedirectUris;
}
public String getScopes() {
return scopes;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-2023 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.
@@ -30,10 +30,12 @@ public interface AuthorizationRepository extends JpaRepository<Authorization, St
Optional<Authorization> findByAuthorizationCodeValue(String authorizationCode);
Optional<Authorization> findByAccessTokenValue(String accessToken);
Optional<Authorization> findByRefreshTokenValue(String refreshToken);
Optional<Authorization> findByOidcIdTokenValue(String idToken);
@Query("select a from Authorization a where a.state = :token" +
" or a.authorizationCodeValue = :token" +
" or a.accessTokenValue = :token" +
" or a.refreshTokenValue = :token"
" or a.refreshTokenValue = :token" +
" or a.oidcIdTokenValue = :token"
)
Optional<Authorization> findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValue(@Param("token") String token);
Optional<Authorization> findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValueOrOidcIdTokenValue(@Param("token") String token);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-2023 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.
@@ -35,6 +35,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -88,7 +89,7 @@ public class JpaOAuth2AuthorizationService implements OAuth2AuthorizationService
Optional<Authorization> result;
if (tokenType == null) {
result = this.authorizationRepository.findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValue(token);
result = this.authorizationRepository.findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValueOrOidcIdTokenValue(token);
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
result = this.authorizationRepository.findByState(token);
} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
@@ -97,6 +98,8 @@ public class JpaOAuth2AuthorizationService implements OAuth2AuthorizationService
result = this.authorizationRepository.findByAccessTokenValue(token);
} else if (OAuth2ParameterNames.REFRESH_TOKEN.equals(tokenType.getValue())) {
result = this.authorizationRepository.findByRefreshTokenValue(token);
} else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
result = this.authorizationRepository.findByOidcIdTokenValue(token);
} else {
result = Optional.empty();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2022 the original author or authors.
* Copyright 2022-2023 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.
@@ -78,6 +78,8 @@ public class JpaRegisteredClientRepository implements RegisteredClientRepository
client.getAuthorizationGrantTypes());
Set<String> redirectUris = StringUtils.commaDelimitedListToSet(
client.getRedirectUris());
Set<String> postLogoutRedirectUris = StringUtils.commaDelimitedListToSet(
client.getPostLogoutRedirectUris());
Set<String> clientScopes = StringUtils.commaDelimitedListToSet(
client.getScopes());
@@ -94,6 +96,7 @@ public class JpaRegisteredClientRepository implements RegisteredClientRepository
authorizationGrantTypes.forEach(grantType ->
grantTypes.add(resolveAuthorizationGrantType(grantType))))
.redirectUris((uris) -> uris.addAll(redirectUris))
.postLogoutRedirectUris((uris) -> uris.addAll(postLogoutRedirectUris))
.scopes((scopes) -> scopes.addAll(clientScopes));
Map<String, Object> clientSettingsMap = parseMap(client.getClientSettings());
@@ -124,6 +127,7 @@ public class JpaRegisteredClientRepository implements RegisteredClientRepository
entity.setClientAuthenticationMethods(StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods));
entity.setAuthorizationGrantTypes(StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes));
entity.setRedirectUris(StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris()));
entity.setPostLogoutRedirectUris(StringUtils.collectionToCommaDelimitedString(registeredClient.getPostLogoutRedirectUris()));
entity.setScopes(StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes()));
entity.setClientSettings(writeMap(registeredClient.getClientSettings().getSettings()));
entity.setTokenSettings(writeMap(registeredClient.getTokenSettings().getSettings()));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -102,7 +102,8 @@ public class SecurityConfigTests {
assertThatAuthorization(refreshToken, null).isNotNull();
String idToken = (String) tokenResponse.get(OidcParameterNames.ID_TOKEN);
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNull(); // id_token is not searchable
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNotNull();
assertThatAuthorization(idToken, null).isNotNull();
OAuth2Authorization authorization = findAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN);
assertThat(authorization.getToken(idToken)).isNotNull();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -117,7 +117,8 @@ public class JpaTests {
assertThatAuthorization(refreshToken, null).isNotNull();
String idToken = (String) tokenResponse.get(OidcParameterNames.ID_TOKEN);
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNull(); // id_token is not searchable
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNotNull();
assertThatAuthorization(idToken, null).isNotNull();
OAuth2Authorization authorization = findAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN);
assertThat(authorization.getToken(idToken)).isNotNull();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -37,6 +37,7 @@ public class RegisteredClients {
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/authorized")
.postLogoutRedirectUri("http://127.0.0.1:8080/index")
.scope(OidcScopes.OPENID)
.scope("message.read")
.scope("message.write")

View File

@@ -45,6 +45,7 @@ CREATE TABLE client (
clientAuthenticationMethods varchar(1000) NOT NULL,
authorizationGrantTypes varchar(1000) NOT NULL,
redirectUris varchar(1000) DEFAULT NULL,
postLogoutRedirectUris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
clientSettings varchar(2000) NOT NULL,
tokenSettings varchar(2000) NOT NULL,

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -26,6 +26,8 @@ import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.util.Assert;
/**
@@ -150,6 +152,7 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
return matchesState(authorization, token) ||
matchesAuthorizationCode(authorization, token) ||
matchesAccessToken(authorization, token) ||
matchesIdToken(authorization, token) ||
matchesRefreshToken(authorization, token);
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
return matchesState(authorization, token);
@@ -157,6 +160,8 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
return matchesAuthorizationCode(authorization, token);
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
return matchesAccessToken(authorization, token);
} else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
return matchesIdToken(authorization, token);
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
return matchesRefreshToken(authorization, token);
}
@@ -185,6 +190,12 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
return refreshToken != null && refreshToken.getToken().getTokenValue().equals(token);
}
private static boolean matchesIdToken(OAuth2Authorization authorization, String token) {
OAuth2Authorization.Token<OidcIdToken> idToken =
authorization.getToken(OidcIdToken.class);
return idToken != null && idToken.getToken().getTokenValue().equals(token);
}
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -53,6 +53,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
@@ -112,11 +113,12 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
private static final String PK_FILTER = "id = ?";
private static final String UNKNOWN_TOKEN_TYPE_FILTER = "state = ? OR authorization_code_value = ? OR " +
"access_token_value = ? OR refresh_token_value = ?";
"access_token_value = ? OR oidc_id_token_value = ? OR refresh_token_value = ?";
private static final String STATE_FILTER = "state = ?";
private static final String AUTHORIZATION_CODE_FILTER = "authorization_code_value = ?";
private static final String ACCESS_TOKEN_FILTER = "access_token_value = ?";
private static final String ID_TOKEN_FILTER = "oidc_id_token_value = ?";
private static final String REFRESH_TOKEN_FILTER = "refresh_token_value = ?";
// @formatter:off
@@ -240,6 +242,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
parameters.add(new SqlParameterValue(Types.VARCHAR, token));
parameters.add(mapToSqlParameter("authorization_code_value", token));
parameters.add(mapToSqlParameter("access_token_value", token));
parameters.add(mapToSqlParameter("oidc_id_token_value", token));
parameters.add(mapToSqlParameter("refresh_token_value", token));
return findBy(UNKNOWN_TOKEN_TYPE_FILTER, parameters);
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
@@ -251,6 +254,9 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
parameters.add(mapToSqlParameter("access_token_value", token));
return findBy(ACCESS_TOKEN_FILTER, parameters);
} else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
parameters.add(mapToSqlParameter("oidc_id_token_value", token));
return findBy(ID_TOKEN_FILTER, parameters);
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
parameters.add(mapToSqlParameter("refresh_token_value", token));
return findBy(REFRESH_TOKEN_FILTER, parameters);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -16,8 +16,11 @@
package org.springframework.security.oauth2.server.authorization.authentication;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
@@ -27,6 +30,8 @@ import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@@ -52,6 +57,7 @@ import org.springframework.security.oauth2.server.authorization.token.DefaultOAu
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
@@ -79,6 +85,7 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
private final Log logger = LogFactory.getLog(getClass());
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
private SessionRegistry sessionRegistry;
/**
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters.
@@ -149,10 +156,12 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
this.logger.trace("Validated token request parameters");
}
Authentication principal = authorization.getAttribute(Principal.class.getName());
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(authorization.getAttribute(Principal.class.getName()))
.principal(principal)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorization(authorization)
.authorizedScopes(authorization.getAuthorizedScopes())
@@ -210,6 +219,10 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
// ----- ID token -----
OidcIdToken idToken;
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
SessionInformation sessionInformation = getSessionInformation(principal);
if (sessionInformation != null) {
tokenContextBuilder.put(SessionInformation.class, sessionInformation);
}
// @formatter:off
tokenContext = tokenContextBuilder
.tokenType(ID_TOKEN_TOKEN_TYPE)
@@ -265,4 +278,32 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Sets the {@link SessionRegistry} used to track OpenID Connect sessions.
*
* @param sessionRegistry the {@link SessionRegistry} used to track OpenID Connect sessions
* @since 1.1.0
*/
public void setSessionRegistry(SessionRegistry sessionRegistry) {
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.sessionRegistry = sessionRegistry;
}
private SessionInformation getSessionInformation(Authentication principal) {
SessionInformation sessionInformation = null;
if (this.sessionRegistry != null) {
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(principal.getPrincipal(), false);
if (!CollectionUtils.isEmpty(sessions)) {
sessionInformation = sessions.get(0);
if (sessions.size() > 1) {
// Get the most recent session
sessions = new ArrayList<>(sessions);
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
sessionInformation = sessions.get(sessions.size() - 1);
}
}
}
return sessionInformation;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -77,6 +77,7 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
+ "client_authentication_methods, "
+ "authorization_grant_types, "
+ "redirect_uris, "
+ "post_logout_redirect_uris, "
+ "scopes, "
+ "client_settings,"
+ "token_settings";
@@ -90,13 +91,13 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
// @formatter:off
private static final String INSERT_REGISTERED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
+ "(" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ "(" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
// @formatter:on
// @formatter:off
private static final String UPDATE_REGISTERED_CLIENT_SQL = "UPDATE " + TABLE_NAME
+ " SET client_name = ?, client_authentication_methods = ?, authorization_grant_types = ?,"
+ " redirect_uris = ?, scopes = ?, client_settings = ?, token_settings = ?"
+ " redirect_uris = ?, post_logout_redirect_uris = ?, scopes = ?, client_settings = ?, token_settings = ?"
+ " WHERE " + PK_FILTER;
// @formatter:on
@@ -241,6 +242,7 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
Set<String> clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(rs.getString("client_authentication_methods"));
Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorization_grant_types"));
Set<String> redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirect_uris"));
Set<String> postLogoutRedirectUris = StringUtils.commaDelimitedListToSet(rs.getString("post_logout_redirect_uris"));
Set<String> clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"));
// @formatter:off
@@ -257,6 +259,7 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
authorizationGrantTypes.forEach(grantType ->
grantTypes.add(resolveAuthorizationGrantType(grantType))))
.redirectUris((uris) -> uris.addAll(redirectUris))
.postLogoutRedirectUris((uris) -> uris.addAll(postLogoutRedirectUris))
.scopes((scopes) -> scopes.addAll(clientScopes));
// @formatter:on
@@ -354,6 +357,7 @@ public class JdbcRegisteredClientRepository implements RegisteredClientRepositor
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods)),
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes)),
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris())),
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(registeredClient.getPostLogoutRedirectUris())),
new SqlParameterValue(Types.VARCHAR, StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes())),
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getClientSettings().getSettings())),
new SqlParameterValue(Types.VARCHAR, writeMap(registeredClient.getTokenSettings().getSettings())));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -54,6 +54,7 @@ public class RegisteredClient implements Serializable {
private Set<ClientAuthenticationMethod> clientAuthenticationMethods;
private Set<AuthorizationGrantType> authorizationGrantTypes;
private Set<String> redirectUris;
private Set<String> postLogoutRedirectUris;
private Set<String> scopes;
private ClientSettings clientSettings;
private TokenSettings tokenSettings;
@@ -145,6 +146,18 @@ public class RegisteredClient implements Serializable {
return this.redirectUris;
}
/**
* Returns the post logout redirect URI(s) that the client may use for logout.
* The {@code post_logout_redirect_uri} parameter is used by the client when requesting
* that the End-User's User Agent be redirected to after a logout has been performed.
*
* @return the {@code Set} of post logout redirect URI(s)
* @since 1.1.0
*/
public Set<String> getPostLogoutRedirectUris() {
return this.postLogoutRedirectUris;
}
/**
* Returns the scope(s) that the client may use.
*
@@ -190,6 +203,7 @@ public class RegisteredClient implements Serializable {
Objects.equals(this.clientAuthenticationMethods, that.clientAuthenticationMethods) &&
Objects.equals(this.authorizationGrantTypes, that.authorizationGrantTypes) &&
Objects.equals(this.redirectUris, that.redirectUris) &&
Objects.equals(this.postLogoutRedirectUris, that.postLogoutRedirectUris) &&
Objects.equals(this.scopes, that.scopes) &&
Objects.equals(this.clientSettings, that.clientSettings) &&
Objects.equals(this.tokenSettings, that.tokenSettings);
@@ -199,7 +213,7 @@ public class RegisteredClient implements Serializable {
public int hashCode() {
return Objects.hash(this.id, this.clientId, this.clientIdIssuedAt, this.clientSecret, this.clientSecretExpiresAt,
this.clientName, this.clientAuthenticationMethods, this.authorizationGrantTypes, this.redirectUris,
this.scopes, this.clientSettings, this.tokenSettings);
this.postLogoutRedirectUris, this.scopes, this.clientSettings, this.tokenSettings);
}
@Override
@@ -211,6 +225,7 @@ public class RegisteredClient implements Serializable {
", clientAuthenticationMethods=" + this.clientAuthenticationMethods +
", authorizationGrantTypes=" + this.authorizationGrantTypes +
", redirectUris=" + this.redirectUris +
", postLogoutRedirectUris=" + this.postLogoutRedirectUris +
", scopes=" + this.scopes +
", clientSettings=" + this.clientSettings +
", tokenSettings=" + this.tokenSettings +
@@ -253,6 +268,7 @@ public class RegisteredClient implements Serializable {
private final Set<ClientAuthenticationMethod> clientAuthenticationMethods = new HashSet<>();
private final Set<AuthorizationGrantType> authorizationGrantTypes = new HashSet<>();
private final Set<String> redirectUris = new HashSet<>();
private final Set<String> postLogoutRedirectUris = new HashSet<>();
private final Set<String> scopes = new HashSet<>();
private ClientSettings clientSettings;
private TokenSettings tokenSettings;
@@ -277,6 +293,9 @@ public class RegisteredClient implements Serializable {
if (!CollectionUtils.isEmpty(registeredClient.getRedirectUris())) {
this.redirectUris.addAll(registeredClient.getRedirectUris());
}
if (!CollectionUtils.isEmpty(registeredClient.getPostLogoutRedirectUris())) {
this.postLogoutRedirectUris.addAll(registeredClient.getPostLogoutRedirectUris());
}
if (!CollectionUtils.isEmpty(registeredClient.getScopes())) {
this.scopes.addAll(registeredClient.getScopes());
}
@@ -421,6 +440,33 @@ public class RegisteredClient implements Serializable {
return this;
}
/**
* Adds a post logout redirect URI the client may use for logout.
* The {@code post_logout_redirect_uri} parameter is used by the client when requesting
* that the End-User's User Agent be redirected to after a logout has been performed.
*
* @param postLogoutRedirectUri the post logout redirect URI
* @return the {@link Builder}
* @since 1.1.0
*/
public Builder postLogoutRedirectUri(String postLogoutRedirectUri) {
this.postLogoutRedirectUris.add(postLogoutRedirectUri);
return this;
}
/**
* A {@code Consumer} of the post logout redirect URI(s)
* allowing the ability to add, replace, or remove.
*
* @param postLogoutRedirectUrisConsumer a {@link Consumer} of the post logout redirect URI(s)
* @return the {@link Builder}
* @since 1.1.0
*/
public Builder postLogoutRedirectUris(Consumer<Set<String>> postLogoutRedirectUrisConsumer) {
postLogoutRedirectUrisConsumer.accept(this.postLogoutRedirectUris);
return this;
}
/**
* Adds a scope the client may use.
*
@@ -499,6 +545,7 @@ public class RegisteredClient implements Serializable {
}
validateScopes();
validateRedirectUris();
validatePostLogoutRedirectUris();
return create();
}
@@ -523,6 +570,8 @@ public class RegisteredClient implements Serializable {
new HashSet<>(this.authorizationGrantTypes));
registeredClient.redirectUris = Collections.unmodifiableSet(
new HashSet<>(this.redirectUris));
registeredClient.postLogoutRedirectUris = Collections.unmodifiableSet(
new HashSet<>(this.postLogoutRedirectUris));
registeredClient.scopes = Collections.unmodifiableSet(
new HashSet<>(this.scopes));
registeredClient.clientSettings = this.clientSettings;
@@ -557,12 +606,23 @@ public class RegisteredClient implements Serializable {
return;
}
for (String redirectUri : redirectUris) {
for (String redirectUri : this.redirectUris) {
Assert.isTrue(validateRedirectUri(redirectUri),
"redirect_uri \"" + redirectUri + "\" is not a valid redirect URI or contains fragment");
}
}
private void validatePostLogoutRedirectUris() {
if (CollectionUtils.isEmpty(this.postLogoutRedirectUris)) {
return;
}
for (String postLogoutRedirectUri : this.postLogoutRedirectUris) {
Assert.isTrue(validateRedirectUri(postLogoutRedirectUri),
"post_logout_redirect_uri \"" + postLogoutRedirectUri + "\" is not a valid post logout redirect URI or contains fragment");
}
}
private static boolean validateRedirectUri(String redirectUri) {
try {
URI validRedirectUri = new URI(redirectUri);
@@ -571,5 +631,6 @@ public class RegisteredClient implements Serializable {
return false;
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -44,6 +44,7 @@ import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -68,6 +69,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
private AuthenticationFailureHandler errorResponseHandler;
private String consentPage;
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator;
private SessionAuthenticationStrategy sessionAuthenticationStrategy;
/**
* Restrict for internal use only.
@@ -200,6 +202,10 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
this.authorizationCodeRequestAuthenticationValidator.andThen(authenticationValidator);
}
void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
@@ -245,6 +251,9 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
if (StringUtils.hasText(this.consentPage)) {
authorizationEndpointFilter.setConsentPage(this.consentPage);
}
if (this.sessionAuthenticationStrategy != null) {
authorizationEndpointFilter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy);
}
httpSecurity.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -29,6 +29,8 @@ import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2Token;
@@ -240,8 +242,23 @@ public final class OAuth2AuthorizationServerConfigurer
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
validateAuthorizationServerSettings(authorizationServerSettings);
OidcConfigurer oidcConfigurer = getConfigurer(OidcConfigurer.class);
if (oidcConfigurer == null) {
if (isOidcEnabled()) {
// Add OpenID Connect session tracking capabilities.
SessionRegistry sessionRegistry = OAuth2ConfigurerUtils.getSessionRegistry(httpSecurity);
OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer =
getConfigurer(OAuth2AuthorizationEndpointConfigurer.class);
authorizationEndpointConfigurer.setSessionAuthenticationStrategy((authentication, request, response) -> {
if (authentication instanceof OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
if (sessionRegistry.getSessionInformation(request.getSession().getId()) == null) {
sessionRegistry.registerNewSession(
request.getSession().getId(),
((Authentication) authorizationCodeRequestAuthentication.getPrincipal()).getPrincipal());
}
}
}
});
} else {
// OpenID Connect is disabled.
// Add an authentication validator that rejects authentication requests.
OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer =
@@ -297,6 +314,10 @@ public final class OAuth2AuthorizationServerConfigurer
}
}
private boolean isOidcEnabled() {
return getConfigurer(OidcConfigurer.class) != null;
}
private Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> createConfigurers() {
Map<Class<? extends AbstractOAuth2Configurer>, AbstractOAuth2Configurer> configurers = new LinkedHashMap<>();
configurers.put(OAuth2ClientAuthenticationConfigurer.class, new OAuth2ClientAuthenticationConfigurer(this::postProcess));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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,8 +24,14 @@ import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.GenericApplicationListenerAdapter;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.ResolvableType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.context.DelegatingApplicationListener;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
@@ -180,6 +186,28 @@ final class OAuth2ConfigurerUtils {
return authorizationServerSettings;
}
static SessionRegistry getSessionRegistry(HttpSecurity httpSecurity) {
SessionRegistry sessionRegistry = httpSecurity.getSharedObject(SessionRegistry.class);
if (sessionRegistry == null) {
sessionRegistry = getOptionalBean(httpSecurity, SessionRegistry.class);
if (sessionRegistry == null) {
sessionRegistry = new SessionRegistryImpl();
registerDelegateApplicationListener(httpSecurity, (SessionRegistryImpl) sessionRegistry);
}
httpSecurity.setSharedObject(SessionRegistry.class, sessionRegistry);
}
return sessionRegistry;
}
private static void registerDelegateApplicationListener(HttpSecurity httpSecurity, ApplicationListener<?> delegate) {
DelegatingApplicationListener delegatingApplicationListener = getOptionalBean(httpSecurity, DelegatingApplicationListener.class);
if (delegatingApplicationListener == null) {
return;
}
SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
delegatingApplicationListener.addListener(smartListener);
}
static <T> T getBean(HttpSecurity httpSecurity, Class<T> type) {
return httpSecurity.getSharedObject(ApplicationContext.class).getBean(type);
}

View File

@@ -26,6 +26,7 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2Token;
@@ -216,9 +217,11 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity);
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = OAuth2ConfigurerUtils.getTokenGenerator(httpSecurity);
SessionRegistry sessionRegistry = OAuth2ConfigurerUtils.getSessionRegistry(httpSecurity);
OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
new OAuth2AuthorizationCodeAuthenticationProvider(authorizationService, tokenGenerator);
authorizationCodeAuthenticationProvider.setSessionRegistry(sessionRegistry);
authenticationProviders.add(authorizationCodeAuthenticationProvider);
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -37,6 +37,7 @@ import org.springframework.web.util.UriComponentsBuilder;
* @since 0.2.0
* @see OAuth2AuthorizationServerConfigurer#oidc
* @see OidcProviderConfigurationEndpointConfigurer
* @see OidcLogoutEndpointConfigurer
* @see OidcClientRegistrationEndpointConfigurer
* @see OidcUserInfoEndpointConfigurer
*/
@@ -50,6 +51,7 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
OidcConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
addConfigurer(OidcProviderConfigurationEndpointConfigurer.class, new OidcProviderConfigurationEndpointConfigurer(objectPostProcessor));
addConfigurer(OidcLogoutEndpointConfigurer.class, new OidcLogoutEndpointConfigurer(objectPostProcessor));
addConfigurer(OidcUserInfoEndpointConfigurer.class, new OidcUserInfoEndpointConfigurer(objectPostProcessor));
}
@@ -65,6 +67,18 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
return this;
}
/**
* Configures the OpenID Connect 1.0 RP-Initiated Logout Endpoint.
*
* @param logoutEndpointCustomizer the {@link Customizer} providing access to the {@link OidcLogoutEndpointConfigurer}
* @return the {@link OidcConfigurer} for further configuration
* @since 1.1.0
*/
public OidcConfigurer logoutEndpoint(Customizer<OidcLogoutEndpointConfigurer> logoutEndpointCustomizer) {
logoutEndpointCustomizer.customize(getConfigurer(OidcLogoutEndpointConfigurer.class));
return this;
}
/**
* Configures the OpenID Connect Dynamic Client Registration 1.0 Endpoint.
*

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2020-2023 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.config.annotation.web.configurers;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcLogoutEndpointFilter;
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcLogoutAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for OpenID Connect 1.0 RP-Initiated Logout Endpoint.
*
* @author Joe Grandja
* @since 1.1.0
* @see OidcConfigurer#logoutEndpoint
* @see OidcLogoutEndpointFilter
*/
public final class OidcLogoutEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> logoutRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> logoutRequestConvertersConsumer = (logoutRequestConverters) -> {};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {};
private AuthenticationSuccessHandler logoutResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
/**
* Restrict for internal use only.
*/
OidcLogoutEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract a Logout Request from {@link HttpServletRequest}
* to an instance of {@link OidcLogoutAuthenticationToken} used for authenticating the request.
*
* @param logoutRequestConverter an {@link AuthenticationConverter} used when attempting to extract a Logout Request from {@link HttpServletRequest}
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer logoutRequestConverter(
AuthenticationConverter logoutRequestConverter) {
Assert.notNull(logoutRequestConverter, "logoutRequestConverter cannot be null");
this.logoutRequestConverters.add(logoutRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default
* and (optionally) added {@link #logoutRequestConverter(AuthenticationConverter) AuthenticationConverter}'s
* allowing the ability to add, remove, or customize a specific {@link AuthenticationConverter}.
*
* @param logoutRequestConvertersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationConverter}'s
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer logoutRequestConverters(
Consumer<List<AuthenticationConverter>> logoutRequestConvertersConsumer) {
Assert.notNull(logoutRequestConvertersConsumer, "logoutRequestConvertersConsumer cannot be null");
this.logoutRequestConvertersConsumer = logoutRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an {@link OidcLogoutAuthenticationToken}.
*
* @param authenticationProvider an {@link AuthenticationProvider} used for authenticating an {@link OidcLogoutAuthenticationToken}
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer authenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default
* and (optionally) added {@link #authenticationProvider(AuthenticationProvider) AuthenticationProvider}'s
* allowing the ability to add, remove, or customize a specific {@link AuthenticationProvider}.
*
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OidcLogoutAuthenticationToken}
* and performing the logout.
*
* @param logoutResponseHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OidcLogoutAuthenticationToken}
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer logoutResponseHandler(AuthenticationSuccessHandler logoutResponseHandler) {
this.logoutResponseHandler = logoutResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
* and returning the {@link OAuth2Error Error Response}.
*
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
* @return the {@link OidcLogoutEndpointConfigurer} for further configuration
*/
public OidcLogoutEndpointConfigurer errorResponseHandler(AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
String logoutEndpointUri = authorizationServerSettings.getOidcLogoutEndpoint();
this.requestMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.GET.name()),
new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.POST.name())
);
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(authenticationProvider ->
httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils.getAuthorizationServerSettings(httpSecurity);
OidcLogoutEndpointFilter oidcLogoutEndpointFilter =
new OidcLogoutEndpointFilter(
authenticationManager,
authorizationServerSettings.getOidcLogoutEndpoint());
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.logoutRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.logoutRequestConverters);
}
this.logoutRequestConvertersConsumer.accept(authenticationConverters);
oidcLogoutEndpointFilter.setAuthenticationConverter(
new DelegatingAuthenticationConverter(authenticationConverters));
if (this.logoutResponseHandler != null) {
oidcLogoutEndpointFilter.setAuthenticationSuccessHandler(this.logoutResponseHandler);
}
if (this.errorResponseHandler != null) {
oidcLogoutEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterBefore(postProcess(oidcLogoutEndpointFilter), LogoutFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OidcLogoutAuthenticationConverter());
return authenticationConverters;
}
private static List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OidcLogoutAuthenticationProvider oidcLogoutAuthenticationProvider =
new OidcLogoutAuthenticationProvider(
OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
OAuth2ConfigurerUtils.getSessionRegistry(httpSecurity));
authenticationProviders.add(oidcLogoutAuthenticationProvider);
return authenticationProviders;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -37,6 +37,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
* @see OidcClientMetadataClaimNames
* @see OidcClientRegistration
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata">2. Client Metadata</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata">3.1. Client Registration Metadata</a>
*/
public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
@@ -94,6 +95,18 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
return getClaimAsStringList(OidcClientMetadataClaimNames.REDIRECT_URIS);
}
/**
* Returns the post logout redirection {@code URI} values used by the Client {@code (post_logout_redirect_uris)}.
* The {@code post_logout_redirect_uri} parameter is used by the client when requesting
* that the End-User's User Agent be redirected to after a logout has been performed.
*
* @return the post logout redirection {@code URI} values used by the Client
* @since 1.1.0
*/
default List<String> getPostLogoutRedirectUris() {
return getClaimAsStringList(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
}
/**
* Returns the authentication method used by the Client for the Token Endpoint {@code (token_endpoint_auth_method)}.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -28,6 +28,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
* @author Joe Grandja
* @since 0.1.1
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata">2. Client Metadata</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata">3.1. Client Registration Metadata</a>
*/
public final class OidcClientMetadataClaimNames {
@@ -61,6 +62,14 @@ public final class OidcClientMetadataClaimNames {
*/
public static final String REDIRECT_URIS = "redirect_uris";
/**
* {@code post_logout_redirect_uris} - the post logout redirection {@code URI} values used by the Client.
* The {@code post_logout_redirect_uri} parameter is used by the client when requesting
* that the End-User's User Agent be redirected to after a logout has been performed.
* @since 1.1.0
*/
public static final String POST_LOGOUT_REDIRECT_URIS = "post_logout_redirect_uris";
/**
* {@code token_endpoint_auth_method} - the authentication method used by the Client for the Token Endpoint
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -44,8 +44,9 @@ import org.springframework.util.Assert;
* @author Joe Grandja
* @since 0.1.1
* @see OidcClientMetadataClaimAccessor
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest">3.1. Client Registration Request</a>
* @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse">3.2. Client Registration Response</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationRequest">3.1. Client Registration Request</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse">3.2. Client Registration Response</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata">3.1. Client Registration Metadata</a>
*/
public final class OidcClientRegistration implements OidcClientMetadataClaimAccessor, Serializable {
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
@@ -168,6 +169,33 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
return this;
}
/**
* Add the post logout redirection {@code URI} used by the Client, OPTIONAL.
* The {@code post_logout_redirect_uri} parameter is used by the client when requesting
* that the End-User's User Agent be redirected to after a logout has been performed.
*
* @param postLogoutRedirectUri the post logout redirection {@code URI} used by the Client
* @return the {@link Builder} for further configuration
* @since 1.1.0
*/
public Builder postLogoutRedirectUri(String postLogoutRedirectUri) {
addClaimToClaimList(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, postLogoutRedirectUri);
return this;
}
/**
* A {@code Consumer} of the post logout redirection {@code URI} values used by the Client,
* allowing the ability to add, replace, or remove, OPTIONAL.
*
* @param postLogoutRedirectUrisConsumer a {@code Consumer} of the post logout redirection {@code URI} values used by the Client
* @return the {@link Builder} for further configuration
* @since 1.1.0
*/
public Builder postLogoutRedirectUris(Consumer<List<String>> postLogoutRedirectUrisConsumer) {
acceptClaimValues(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, postLogoutRedirectUrisConsumer);
return this;
}
/**
* Sets the authentication method used by the Client for the Token Endpoint, OPTIONAL.
*
@@ -358,6 +386,10 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
Assert.notNull(this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be null");
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.REDIRECT_URIS), "redirect_uris cannot be empty");
if (this.claims.get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS), "post_logout_redirect_uris must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS), "post_logout_redirect_uris cannot be empty");
}
if (this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES) != null) {
Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES), "grant_types must be of type List");
Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.GRANT_TYPES), "grant_types cannot be empty");

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -32,6 +32,7 @@ import org.springframework.util.Assert;
* The claims are defined by the OpenID Connect Discovery 1.0 specification.
*
* @author Daniel Garnier-Moiroux
* @author Joe Grandja
* @since 0.1.0
* @see AbstractOAuth2AuthorizationServerMetadata
* @see OidcProviderMetadataClaimAccessor
@@ -130,6 +131,17 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization
return claim(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, userInfoEndpoint);
}
/**
* Use this {@code end_session_endpoint} in the resulting {@link OidcProviderConfiguration}, OPTIONAL.
*
* @param endSessionEndpoint the {@code URL} of the OpenID Connect 1.0 End Session Endpoint
* @return the {@link Builder} for further configuration
* @since 1.1.0
*/
public Builder endSessionEndpoint(String endSessionEndpoint) {
return claim(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT, endSessionEndpoint);
}
/**
* Validate the claims and build the {@link OidcProviderConfiguration}.
* <p>
@@ -159,6 +171,9 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization
if (getClaims().get(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT) != null) {
validateURL(getClaims().get(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT), "userInfoEndpoint must be a valid URL");
}
if (getClaims().get(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT) != null) {
validateURL(getClaims().get(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT), "endSessionEndpoint must be a valid URL");
}
}
@SuppressWarnings("unchecked")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -15,7 +15,6 @@
*/
package org.springframework.security.oauth2.server.authorization.oidc;
import java.net.URL;
import java.util.List;
@@ -30,6 +29,7 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
* in the OpenID Provider Configuration Response.
*
* @author Daniel Garnier-Moiroux
* @author Joe Grandja
* @since 0.1.0
* @see ClaimAccessor
* @see OAuth2AuthorizationServerMetadataClaimAccessor
@@ -68,4 +68,14 @@ public interface OidcProviderMetadataClaimAccessor extends OAuth2AuthorizationSe
return getClaimAsURL(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT);
}
/**
* Returns the {@code URL} of the OpenID Connect 1.0 End Session Endpoint {@code (end_session_endpoint)}.
*
* @return the {@code URL} of the OpenID Connect 1.0 End Session Endpoint
* @since 1.1.0
*/
default URL getEndSessionEndpoint() {
return getClaimAsURL(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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 org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
* in the OpenID Provider Configuration Response.
*
* @author Daniel Garnier-Moiroux
* @author Joe Grandja
* @since 0.1.0
* @see OAuth2AuthorizationServerMetadataClaimNames
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata">3. OpenID Provider Metadata</a>
@@ -46,6 +47,12 @@ public final class OidcProviderMetadataClaimNames extends OAuth2AuthorizationSer
*/
public static final String USER_INFO_ENDPOINT = "userinfo_endpoint";
/**
* {@code end_session_endpoint} - the {@code URL} of the OpenID Connect 1.0 End Session Endpoint
* @since 1.1.0
*/
public static final String END_SESSION_ENDPOINT = "end_session_endpoint";
private OidcProviderMetadataClaimNames() {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -174,6 +174,10 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI, OidcClientMetadataClaimNames.REDIRECT_URIS);
}
if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getPostLogoutRedirectUris())) {
throwInvalidClientRegistration("invalid_client_metadata", OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
}
if (!isValidTokenEndpointAuthenticationMethod(clientRegistrationAuthentication.getClientRegistration())) {
throwInvalidClientRegistration("invalid_client_metadata", OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
}
@@ -371,6 +375,11 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
builder.redirectUris(redirectUris ->
redirectUris.addAll(clientRegistration.getRedirectUris()));
if (!CollectionUtils.isEmpty(clientRegistration.getPostLogoutRedirectUris())) {
builder.postLogoutRedirectUris(postLogoutRedirectUris ->
postLogoutRedirectUris.addAll(clientRegistration.getPostLogoutRedirectUris()));
}
if (!CollectionUtils.isEmpty(clientRegistration.getGrantTypes())) {
builder.authorizationGrantTypes(authorizationGrantTypes ->
clientRegistration.getGrantTypes().forEach(grantType ->

View File

@@ -0,0 +1,181 @@
/*
* Copyright 2020-2023 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.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
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.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* An {@link AuthenticationProvider} implementation for OpenID Connect 1.0 RP-Initiated Logout Endpoint.
*
* @author Joe Grandja
* @since 1.1.0
* @see RegisteredClientRepository
* @see OAuth2AuthorizationService
* @see SessionRegistry
* @see <a href="https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout">2. RP-Initiated Logout</a>
*/
public final class OidcLogoutAuthenticationProvider implements AuthenticationProvider {
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE =
new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private final Log logger = LogFactory.getLog(getClass());
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private final SessionRegistry sessionRegistry;
/**
* Constructs an {@code OidcLogoutAuthenticationProvider} using the provided parameters.
*
* @param registeredClientRepository the repository of registered clients
* @param authorizationService the authorization service
* @param sessionRegistry the {@link SessionRegistry} used to track OpenID Connect sessions
*/
public OidcLogoutAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
OAuth2AuthorizationService authorizationService, SessionRegistry sessionRegistry) {
Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
this.registeredClientRepository = registeredClientRepository;
this.authorizationService = authorizationService;
this.sessionRegistry = sessionRegistry;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OidcLogoutAuthenticationToken oidcLogoutAuthentication =
(OidcLogoutAuthenticationToken) authentication;
OAuth2Authorization authorization = this.authorizationService.findByToken(
oidcLogoutAuthentication.getIdToken(), ID_TOKEN_TOKEN_TYPE);
if (authorization == null) {
throwError(OAuth2ErrorCodes.INVALID_TOKEN, "id_token_hint");
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved authorization with ID Token");
}
RegisteredClient registeredClient = this.registeredClientRepository.findById(
authorization.getRegisteredClientId());
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved registered client");
}
OidcIdToken idToken = authorization.getToken(OidcIdToken.class).getToken();
// Validate client identity
List<String> audClaim = idToken.getAudience();
if (CollectionUtils.isEmpty(audClaim) ||
!audClaim.contains(registeredClient.getClientId())) {
throwError(OAuth2ErrorCodes.INVALID_TOKEN, IdTokenClaimNames.AUD);
}
if (StringUtils.hasText(oidcLogoutAuthentication.getClientId()) &&
!oidcLogoutAuthentication.getClientId().equals(registeredClient.getClientId())) {
throwError(OAuth2ErrorCodes.INVALID_TOKEN, OAuth2ParameterNames.CLIENT_ID);
}
if (StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri()) &&
!registeredClient.getPostLogoutRedirectUris().contains(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "post_logout_redirect_uri");
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated logout request parameters");
}
// Validate user session
SessionInformation sessionInformation = null;
Authentication userPrincipal = (Authentication) oidcLogoutAuthentication.getPrincipal();
if (isPrincipalAuthenticated(userPrincipal) &&
StringUtils.hasText(oidcLogoutAuthentication.getSessionId())) {
sessionInformation = findSessionInformation(
userPrincipal, oidcLogoutAuthentication.getSessionId());
if (sessionInformation != null) {
String sidClaim = idToken.getClaim("sid");
if (!StringUtils.hasText(sidClaim) ||
!sidClaim.equals(sessionInformation.getSessionId())) {
throwError(OAuth2ErrorCodes.INVALID_TOKEN, "sid");
}
}
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated logout request");
}
return new OidcLogoutAuthenticationToken(oidcLogoutAuthentication.getIdToken(), userPrincipal,
sessionInformation, oidcLogoutAuthentication.getClientId(),
oidcLogoutAuthentication.getPostLogoutRedirectUri(), oidcLogoutAuthentication.getState());
}
@Override
public boolean supports(Class<?> authentication) {
return OidcLogoutAuthenticationToken.class.isAssignableFrom(authentication);
}
private static boolean isPrincipalAuthenticated(Authentication principal) {
return principal != null &&
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
principal.isAuthenticated();
}
private SessionInformation findSessionInformation(Authentication principal, String sessionId) {
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(principal.getPrincipal(), true);
SessionInformation sessionInformation = null;
if (!CollectionUtils.isEmpty(sessions)) {
for (SessionInformation session : sessions) {
if (session.getSessionId().equals(sessionId)) {
sessionInformation = session;
break;
}
}
}
return sessionInformation;
}
private static void throwError(String errorCode, String parameterName) {
OAuth2Error error = new OAuth2Error(
errorCode,
"OpenID Connect 1.0 Logout Request Parameter: " + parameterName,
"https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ValidationAndErrorHandling");
throw new OAuth2AuthenticationException(error);
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2020-2023 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.Collections;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
import org.springframework.util.Assert;
/**
* An {@link Authentication} implementation used for OpenID Connect 1.0 RP-Initiated Logout Endpoint.
*
* @author Joe Grandja
* @since 1.1.0
* @see AbstractAuthenticationToken
* @see OidcLogoutAuthenticationProvider
*/
public class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
private final String idToken;
private final Authentication principal;
private final String sessionId;
private final SessionInformation sessionInformation;
private final String clientId;
private final String postLogoutRedirectUri;
private final String state;
/**
* Constructs an {@code OidcLogoutAuthenticationToken} using the provided parameters.
*
* @param idToken the ID Token previously issued by the Provider to the Client and used as a hint about the End-User's current authenticated session with the Client
* @param principal the authenticated principal representing the End-User
* @param sessionId the End-User's current authenticated session identifier with the Client
* @param clientId the client identifier the ID Token was issued to
* @param postLogoutRedirectUri the URI which the Client is requesting that the End-User's User Agent be redirected to after a logout has been performed
* @param state the opaque value used by the Client to maintain state between the logout request and the callback to the {@code postLogoutRedirectUri}
*/
public OidcLogoutAuthenticationToken(String idToken, Authentication principal, @Nullable String sessionId,
@Nullable String clientId, @Nullable String postLogoutRedirectUri, @Nullable String state) {
super(Collections.emptyList());
Assert.hasText(idToken, "idToken cannot be empty");
Assert.notNull(principal, "principal cannot be null");
this.idToken = idToken;
this.principal = principal;
this.sessionId = sessionId;
this.sessionInformation = null;
this.clientId = clientId;
this.postLogoutRedirectUri = postLogoutRedirectUri;
this.state = state;
setAuthenticated(false);
}
/**
* Constructs an {@code OidcLogoutAuthenticationToken} using the provided parameters.
*
* @param idToken the ID Token previously issued by the Provider to the Client and used as a hint about the End-User's current authenticated session with the Client
* @param principal the authenticated principal representing the End-User
* @param sessionInformation the End-User's current authenticated session information with the Client
* @param clientId the client identifier the ID Token was issued to
* @param postLogoutRedirectUri the URI which the Client is requesting that the End-User's User Agent be redirected to after a logout has been performed
* @param state the opaque value used by the Client to maintain state between the logout request and the callback to the {@code postLogoutRedirectUri}
*/
public OidcLogoutAuthenticationToken(String idToken, Authentication principal, @Nullable SessionInformation sessionInformation,
@Nullable String clientId, @Nullable String postLogoutRedirectUri, @Nullable String state) {
super(Collections.emptyList());
Assert.hasText(idToken, "idToken cannot be empty");
Assert.notNull(principal, "principal cannot be null");
this.idToken = idToken;
this.principal = principal;
this.sessionId = sessionInformation != null ? sessionInformation.getSessionId() : null;
this.sessionInformation = sessionInformation;
this.clientId = clientId;
this.postLogoutRedirectUri = postLogoutRedirectUri;
this.state = state;
setAuthenticated(true);
}
/**
* Returns the authenticated principal representing the End-User.
*
* @return the authenticated principal
*/
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the ID Token previously issued by the Provider to the Client and used as a hint
* about the End-User's current authenticated session with the Client.
*
* @return the ID Token previously issued by the Provider to the Client
*/
public String getIdToken() {
return this.idToken;
}
/**
* Returns the End-User's current authenticated session identifier with the Client.
*
* @return the End-User's current authenticated session identifier
*/
@Nullable
public String getSessionId() {
return this.sessionId;
}
/**
* Returns the End-User's current authenticated session information with the Client.
*
* @return the End-User's current authenticated session information
*/
@Nullable
public SessionInformation getSessionInformation() {
return this.sessionInformation;
}
/**
* Returns the client identifier the ID Token was issued to.
*
* @return the client identifier
*/
@Nullable
public String getClientId() {
return this.clientId;
}
/**
* Returns the URI which the Client is requesting that the End-User's User Agent be redirected to after a logout has been performed.
*
* @return the URI which the Client is requesting that the End-User's User Agent be redirected to after a logout has been performed
*/
@Nullable
public String getPostLogoutRedirectUri() {
return this.postLogoutRedirectUri;
}
/**
* Returns the opaque value used by the Client to maintain state between the logout request and the callback to the {@link #getPostLogoutRedirectUri()}.
*
* @return the opaque value used by the Client to maintain state between the logout request and the callback to the {@link #getPostLogoutRedirectUri()}
*/
@Nullable
public String getState() {
return this.state;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -48,6 +48,11 @@ final class RegisteredClientOidcClientRegistrationConverter implements Converter
builder.redirectUris(redirectUris ->
redirectUris.addAll(registeredClient.getRedirectUris()));
if (!CollectionUtils.isEmpty(registeredClient.getPostLogoutRedirectUris())) {
builder.postLogoutRedirectUris(postLogoutRedirectUris ->
postLogoutRedirectUris.addAll(registeredClient.getPostLogoutRedirectUris()));
}
builder.grantTypes(grantTypes ->
registeredClient.getAuthorizationGrantTypes().forEach(authorizationGrantType ->
grantTypes.add(authorizationGrantType.getValue())));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -148,6 +148,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
claimConverters.put(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, MapOidcClientRegistrationConverter::convertClientSecretExpiresAt);
claimConverters.put(OidcClientMetadataClaimNames.CLIENT_NAME, stringConverter);
claimConverters.put(OidcClientMetadataClaimNames.REDIRECT_URIS, collectionStringConverter);
claimConverters.put(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, collectionStringConverter);
claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, stringConverter);
claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, stringConverter);
claimConverters.put(OidcClientMetadataClaimNames.GRANT_TYPES, collectionStringConverter);

View File

@@ -0,0 +1,218 @@
/*
* Copyright 2020-2023 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.web;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcLogoutAuthenticationConverter;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* A {@code Filter} that processes OpenID Connect 1.0 RP-Initiated Logout Requests.
*
* @author Joe Grandja
* @since 1.1.0
* @see OidcLogoutAuthenticationConverter
* @see OidcLogoutAuthenticationProvider
* @see <a href="https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout">2. RP-Initiated Logout</a>
*/
public final class OidcLogoutEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for OpenID Connect 1.0 RP-Initiated Logout Requests.
*/
private static final String DEFAULT_OIDC_LOGOUT_ENDPOINT_URI = "/connect/logout";
private final AuthenticationManager authenticationManager;
private final RequestMatcher logoutEndpointMatcher;
private final LogoutHandler logoutHandler;
private final LogoutSuccessHandler logoutSuccessHandler;
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private AuthenticationConverter authenticationConverter;
private AuthenticationSuccessHandler authenticationSuccessHandler = this::performLogout;
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
/**
* Constructs an {@code OidcLogoutEndpointFilter} using the provided parameters.
*
* @param authenticationManager the authentication manager
*/
public OidcLogoutEndpointFilter(AuthenticationManager authenticationManager) {
this(authenticationManager, DEFAULT_OIDC_LOGOUT_ENDPOINT_URI);
}
/**
* Constructs an {@code OidcLogoutEndpointFilter} using the provided parameters.
*
* @param authenticationManager the authentication manager
* @param logoutEndpointUri the endpoint {@code URI} for OpenID Connect 1.0 RP-Initiated Logout Requests
*/
public OidcLogoutEndpointFilter(AuthenticationManager authenticationManager,
String logoutEndpointUri) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.hasText(logoutEndpointUri, "logoutEndpointUri cannot be empty");
this.authenticationManager = authenticationManager;
this.logoutEndpointMatcher = new OrRequestMatcher(
new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.GET.name()),
new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.POST.name()));
this.logoutHandler = new SecurityContextLogoutHandler();
SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
urlLogoutSuccessHandler.setDefaultTargetUrl("/");
this.logoutSuccessHandler = urlLogoutSuccessHandler;
this.authenticationConverter = new OidcLogoutAuthenticationConverter();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.logoutEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
Authentication oidcLogoutAuthentication = this.authenticationConverter.convert(request);
Authentication oidcLogoutAuthenticationResult =
this.authenticationManager.authenticate(oidcLogoutAuthentication);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, oidcLogoutAuthenticationResult);
} catch (OAuth2AuthenticationException ex) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Logout request failed: %s", ex.getError()), ex);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
} catch (Exception ex) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_REQUEST,
"OpenID Connect 1.0 RP-Initiated Logout Error: " + ex.getMessage(),
"https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ValidationAndErrorHandling");
if (this.logger.isTraceEnabled()) {
this.logger.trace(error, ex);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
new OAuth2AuthenticationException(error));
}
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract a Logout Request from {@link HttpServletRequest}
* to an instance of {@link OidcLogoutAuthenticationToken} used for authenticating the request.
*
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract a Logout Request from {@link HttpServletRequest}
*/
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an {@link OidcLogoutAuthenticationToken}
* and performing the logout.
*
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for handling an {@link OidcLogoutAuthenticationToken}
*/
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
* and returning the {@link OAuth2Error Error Response}.
*
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for handling an {@link OAuth2AuthenticationException}
*/
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}
private void performLogout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
OidcLogoutAuthenticationToken oidcLogoutAuthentication = (OidcLogoutAuthenticationToken) authentication;
// Check for active user session
if (oidcLogoutAuthentication.getSessionInformation() != null) {
// Perform logout
this.logoutHandler.logout(request, response,
(Authentication) oidcLogoutAuthentication.getPrincipal());
}
if (oidcLogoutAuthentication.isAuthenticated() &&
StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
// Perform post-logout redirect
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(oidcLogoutAuthentication.getPostLogoutRedirectUri());
String redirectUri;
if (StringUtils.hasText(oidcLogoutAuthentication.getState())) {
uriBuilder.queryParam(
OAuth2ParameterNames.STATE,
UriUtils.encode(oidcLogoutAuthentication.getState(), StandardCharsets.UTF_8));
}
redirectUri = uriBuilder.build(true).toUriString(); // build(true) -> Components are explicitly encoded
this.redirectStrategy.sendRedirect(request, response, redirectUri);
} else {
// Perform default redirect
this.logoutSuccessHandler.onLogoutSuccess(request, response,
(Authentication) oidcLogoutAuthentication.getPrincipal());
}
}
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
response.sendError(HttpStatus.BAD_REQUEST.value(), error.toString());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -98,6 +98,7 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
.jwkSetUrl(asUrl(issuer, authorizationServerSettings.getJwkSetEndpoint()))
.userInfoEndpoint(asUrl(issuer, authorizationServerSettings.getOidcUserInfoEndpoint()))
.endSessionEndpoint(asUrl(issuer, authorizationServerSettings.getOidcLogoutEndpoint()))
.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2020-2023 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.web.authentication;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
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.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcLogoutEndpointFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Attempts to extract an OpenID Connect 1.0 RP-Initiated Logout Request from {@link HttpServletRequest}
* and then converts to an {@link OidcLogoutAuthenticationToken} used for authenticating the request.
*
* @author Joe Grandja
* @since 1.1.0
* @see AuthenticationConverter
* @see OidcLogoutAuthenticationToken
* @see OidcLogoutEndpointFilter
*/
public final class OidcLogoutAuthenticationConverter implements AuthenticationConverter {
private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
@Override
public Authentication convert(HttpServletRequest request) {
MultiValueMap<String, String> parameters = getParameters(request);
// id_token_hint (REQUIRED) // RECOMMENDED as per spec
String idTokenHint = request.getParameter("id_token_hint");
if (!StringUtils.hasText(idTokenHint) ||
request.getParameterValues("id_token_hint").length != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "id_token_hint");
}
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (principal == null) {
principal = ANONYMOUS_AUTHENTICATION;
}
String sessionId = null;
HttpSession session = request.getSession(false);
if (session != null) {
sessionId = session.getId();
}
// client_id (OPTIONAL)
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
if (StringUtils.hasText(clientId) &&
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
}
// post_logout_redirect_uri (OPTIONAL)
String postLogoutRedirectUri = parameters.getFirst("post_logout_redirect_uri");
if (StringUtils.hasText(postLogoutRedirectUri) &&
parameters.get("post_logout_redirect_uri").size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "post_logout_redirect_uri");
}
// state (OPTIONAL)
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
if (StringUtils.hasText(state) &&
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
}
return new OidcLogoutAuthenticationToken(idTokenHint, principal,
sessionId, clientId, postLogoutRedirectUri, state);
}
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
private static void throwError(String errorCode, String parameterName) {
OAuth2Error error = new OAuth2Error(
errorCode,
"OpenID Connect 1.0 Logout Request Parameter: " + parameterName,
"https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ValidationAndErrorHandling");
throw new OAuth2AuthenticationException(error);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -35,7 +35,7 @@ public final class AuthorizationServerSettings extends AbstractSettings {
}
/**
* Returns the URL of the Authorization Server's Issuer Identifier
* Returns the URL of the Authorization Server's Issuer Identifier.
*
* @return the URL of the Authorization Server's Issuer Identifier
*/
@@ -106,6 +106,16 @@ public final class AuthorizationServerSettings extends AbstractSettings {
return getSetting(ConfigurationSettingNames.AuthorizationServer.OIDC_USER_INFO_ENDPOINT);
}
/**
* Returns the OpenID Connect 1.0 Logout endpoint. The default is {@code /connect/logout}.
*
* @return the OpenID Connect 1.0 Logout endpoint
* @since 1.1.0
*/
public String getOidcLogoutEndpoint() {
return getSetting(ConfigurationSettingNames.AuthorizationServer.OIDC_LOGOUT_ENDPOINT);
}
/**
* Constructs a new {@link Builder} with the default settings.
*
@@ -119,7 +129,8 @@ public final class AuthorizationServerSettings extends AbstractSettings {
.tokenRevocationEndpoint("/oauth2/revoke")
.tokenIntrospectionEndpoint("/oauth2/introspect")
.oidcClientRegistrationEndpoint("/connect/register")
.oidcUserInfoEndpoint("/userinfo");
.oidcUserInfoEndpoint("/userinfo")
.oidcLogoutEndpoint("/connect/logout");
}
/**
@@ -222,6 +233,17 @@ public final class AuthorizationServerSettings extends AbstractSettings {
return setting(ConfigurationSettingNames.AuthorizationServer.OIDC_USER_INFO_ENDPOINT, oidcUserInfoEndpoint);
}
/**
* Sets the OpenID Connect 1.0 Logout endpoint.
*
* @param oidcLogoutEndpoint the OpenID Connect 1.0 Logout endpoint
* @return the {@link Builder} for further configuration
* @since 1.1.0
*/
public Builder oidcLogoutEndpoint(String oidcLogoutEndpoint) {
return setting(ConfigurationSettingNames.AuthorizationServer.OIDC_LOGOUT_ENDPOINT, oidcLogoutEndpoint);
}
/**
* Builds the {@link AuthorizationServerSettings}.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -116,6 +116,12 @@ public final class ConfigurationSettingNames {
*/
public static final String OIDC_USER_INFO_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("oidc-user-info-endpoint");
/**
* Set the OpenID Connect 1.0 Logout endpoint.
* @since 1.1.0
*/
public static final String OIDC_LOGOUT_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("oidc-logout-endpoint");
private AuthorizationServer() {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -20,6 +20,7 @@ import java.time.temporal.ChronoUnit;
import java.util.Collections;
import org.springframework.lang.Nullable;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
@@ -126,7 +127,11 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
claimsBuilder.claim(IdTokenClaimNames.NONCE, nonce);
}
}
// TODO Add 'auth_time' claim
SessionInformation sessionInformation = context.get(SessionInformation.class);
if (sessionInformation != null) {
claimsBuilder.claim("sid", sessionInformation.getSessionId());
claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, sessionInformation.getLastRequest());
}
}
// @formatter:on
@@ -147,6 +152,12 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
if (context.getAuthorizationGrant() != null) {
jwtContextBuilder.authorizationGrant(context.getAuthorizationGrant());
}
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
SessionInformation sessionInformation = context.get(SessionInformation.class);
if (sessionInformation != null) {
jwtContextBuilder.put(SessionInformation.class, sessionInformation);
}
}
// @formatter:on
JwtEncodingContext jwtContext = jwtContextBuilder.build();

View File

@@ -35,6 +35,7 @@ import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
@@ -54,6 +55,7 @@ import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.util.RedirectUrlBuilder;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
@@ -97,6 +99,7 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
private AuthenticationConverter authenticationConverter;
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendAuthorizationResponse;
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
private SessionAuthenticationStrategy sessionAuthenticationStrategy = (authentication, request, response) -> {};
private String consentPage;
/**
@@ -182,6 +185,9 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
return;
}
this.sessionAuthenticationStrategy.onAuthentication(
authenticationResult, request, response);
this.authenticationSuccessHandler.onAuthenticationSuccess(
request, response, authenticationResult);
@@ -238,6 +244,19 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
this.authenticationFailureHandler = authenticationFailureHandler;
}
/**
* Sets the {@link SessionAuthenticationStrategy} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
* before calling the {@link AuthenticationSuccessHandler}.
* If OpenID Connect is enabled, the default implementation tracks OpenID Connect sessions using a {@link SessionRegistry}.
*
* @param sessionAuthenticationStrategy the {@link SessionAuthenticationStrategy} used for handling an {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
* @since 1.1.0
*/
public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
Assert.notNull(sessionAuthenticationStrategy, "sessionAuthenticationStrategy cannot be null");
this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
}
/**
* Specify the URI to redirect Resource Owners to if consent is required. A default consent
* page will be generated when this attribute is not specified.

View File

@@ -8,6 +8,7 @@ CREATE TABLE oauth2_registered_client (
client_authentication_methods varchar(1000) NOT NULL,
authorization_grant_types varchar(1000) NOT NULL,
redirect_uris varchar(1000) DEFAULT NULL,
post_logout_redirect_uris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
client_settings varchar(2000) NOT NULL,
token_settings varchar(2000) NOT NULL,

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -26,6 +26,8 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
@@ -47,6 +49,7 @@ public class InMemoryOAuth2AuthorizationServiceTests {
"code", Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES));
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private InMemoryOAuth2AuthorizationService authorizationService;
@BeforeEach
@@ -263,6 +266,29 @@ public class InMemoryOAuth2AuthorizationServiceTests {
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenWhenIdTokenExistsThenFound() {
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject("subject")
.issuedAt(Instant.now().minusSeconds(60))
.expiresAt(Instant.now())
.build();
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.id(ID)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.token(idToken)
.build();
this.authorizationService.save(authorization);
OAuth2Authorization result = this.authorizationService.findByToken(
idToken.getTokenValue(), ID_TOKEN_TOKEN_TYPE);
assertThat(authorization).isEqualTo(result);
result = this.authorizationService.findByToken(idToken.getTokenValue(), null);
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenWhenRefreshTokenExistsThenFound() {
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", Instant.now());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -49,6 +49,7 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
@@ -76,6 +77,7 @@ public class JdbcOAuth2AuthorizationServiceTests {
private static final String OAUTH2_AUTHORIZATION_SCHEMA_CLOB_DATA_TYPE_SQL_RESOURCE = "org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema-clob-data-type.sql";
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private static final String ID = "id";
private static final RegisteredClient REGISTERED_CLIENT = TestRegisteredClients.registeredClient().build();
private static final String PRINCIPAL_NAME = "principal";
@@ -344,6 +346,32 @@ public class JdbcOAuth2AuthorizationServiceTests {
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenWhenIdTokenExistsThenFound() {
when(this.registeredClientRepository.findById(eq(REGISTERED_CLIENT.getId())))
.thenReturn(REGISTERED_CLIENT);
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject("subject")
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().truncatedTo(ChronoUnit.MILLIS))
.build();
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.id(ID)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.token(idToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
this.authorizationService.save(authorization);
OAuth2Authorization result = this.authorizationService.findByToken(
idToken.getTokenValue(), ID_TOKEN_TOKEN_TYPE);
assertThat(authorization).isEqualTo(result);
result = this.authorizationService.findByToken(idToken.getTokenValue(), null);
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenWhenRefreshTokenExistsThenFound() {
when(this.registeredClientRepository.findById(eq(REGISTERED_CLIENT.getId())))
@@ -494,7 +522,7 @@ public class JdbcOAuth2AuthorizationServiceTests {
private static final String PK_FILTER = "id = ?";
private static final String UNKNOWN_TOKEN_TYPE_FILTER = "state = ? OR authorizationCodeValue = ? OR " +
"accessTokenValue = ? OR refreshTokenValue = ?";
"accessTokenValue = ? OR oidcIdTokenValue = ? OR refreshTokenValue = ?";
// @formatter:off
private static final String LOAD_AUTHORIZATION_SQL = "SELECT " + COLUMN_NAMES
@@ -539,7 +567,7 @@ public class JdbcOAuth2AuthorizationServiceTests {
@Override
public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
return findBy(UNKNOWN_TOKEN_TYPE_FILTER, token, token, token, token);
return findBy(UNKNOWN_TOKEN_TYPE_FILTER, token, token, token, token, token);
}
private OAuth2Authorization findBy(String filter, Object... args) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -19,8 +19,11 @@ import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -31,6 +34,8 @@ import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -95,6 +100,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
private OAuth2TokenGenerator<?> tokenGenerator;
private SessionRegistry sessionRegistry;
private OAuth2AuthorizationCodeAuthenticationProvider authenticationProvider;
@BeforeEach
@@ -116,8 +122,10 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
return delegatingTokenGenerator.generate(context);
}
});
this.sessionRegistry = mock(SessionRegistry.class);
this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
this.authorizationService, this.tokenGenerator);
this.authenticationProvider.setSessionRegistry(this.sessionRegistry);
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().issuer("https://provider.com").build();
AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
}
@@ -146,6 +154,13 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(this.authenticationProvider.supports(OAuth2AuthorizationCodeAuthenticationToken.class)).isTrue();
}
@Test
public void setSessionRegistryWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authenticationProvider.setSessionRegistry(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("sessionRegistry cannot be null");
}
@Test
public void authenticateWhenClientPrincipalNotOAuth2ClientAuthenticationTokenThenThrowOAuth2AuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
@@ -456,6 +471,19 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
when(this.jwtEncoder.encode(any())).thenReturn(createJwt());
Authentication principal = authorization.getAttribute(Principal.class.getName());
List<SessionInformation> sessions = new ArrayList<>();
sessions.add(new SessionInformation(principal.getPrincipal(),
"session3", Date.from(Instant.now())));
sessions.add(new SessionInformation(principal.getPrincipal(),
"session2", Date.from(Instant.now().minus(1, ChronoUnit.HOURS))));
sessions.add(new SessionInformation(principal.getPrincipal(),
"session1", Date.from(Instant.now().minus(2, ChronoUnit.HOURS))));
SessionInformation expectedSession = sessions.get(0); // Most recent
when(this.sessionRegistry.getAllSessions(eq(principal.getPrincipal()), eq(false)))
.thenReturn(sessions);
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -464,7 +492,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
// Access Token context
JwtEncodingContext accessTokenContext = jwtEncodingContextCaptor.getAllValues().get(0);
assertThat(accessTokenContext.getRegisteredClient()).isEqualTo(registeredClient);
assertThat(accessTokenContext.<Authentication>getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName()));
assertThat(accessTokenContext.<Authentication>getPrincipal()).isEqualTo(principal);
assertThat(accessTokenContext.getAuthorization()).isEqualTo(authorization);
assertThat(accessTokenContext.getAuthorization().getAccessToken()).isNull();
assertThat(accessTokenContext.getAuthorizedScopes()).isEqualTo(authorization.getAuthorizedScopes());
@@ -480,13 +508,15 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
// ID Token context
JwtEncodingContext idTokenContext = jwtEncodingContextCaptor.getAllValues().get(1);
assertThat(idTokenContext.getRegisteredClient()).isEqualTo(registeredClient);
assertThat(idTokenContext.<Authentication>getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName()));
assertThat(idTokenContext.<Authentication>getPrincipal()).isEqualTo(principal);
assertThat(idTokenContext.getAuthorization()).isNotEqualTo(authorization);
assertThat(idTokenContext.getAuthorization().getAccessToken()).isNotNull();
assertThat(idTokenContext.getAuthorizedScopes()).isEqualTo(authorization.getAuthorizedScopes());
assertThat(idTokenContext.getTokenType().getValue()).isEqualTo(OidcParameterNames.ID_TOKEN);
assertThat(idTokenContext.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(idTokenContext.<OAuth2AuthorizationGrantAuthenticationToken>getAuthorizationGrant()).isEqualTo(authentication);
SessionInformation sessionInformation = idTokenContext.get(SessionInformation.class);
assertThat(sessionInformation).isNotNull().isSameAs(expectedSession);
assertThat(idTokenContext.getJwsHeader()).isNotNull();
assertThat(idTokenContext.getClaims()).isNotNull();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -294,6 +294,7 @@ public class JdbcRegisteredClientRepositoryTests {
+ "clientAuthenticationMethods, "
+ "authorizationGrantTypes, "
+ "redirectUris, "
+ "postLogoutRedirectUris, "
+ "scopes, "
+ "clientSettings,"
+ "tokenSettings";
@@ -305,7 +306,7 @@ public class JdbcRegisteredClientRepositoryTests {
// @formatter:off
private static final String INSERT_REGISTERED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
// @formatter:on
private CustomJdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
@@ -353,6 +354,7 @@ public class JdbcRegisteredClientRepositoryTests {
Set<String> clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(rs.getString("clientAuthenticationMethods"));
Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorizationGrantTypes"));
Set<String> redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirectUris"));
Set<String> postLogoutRedirectUris = StringUtils.commaDelimitedListToSet(rs.getString("postLogoutRedirectUris"));
Set<String> clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"));
// @formatter:off
@@ -369,6 +371,7 @@ public class JdbcRegisteredClientRepositoryTests {
authorizationGrantTypes.forEach(grantType ->
grantTypes.add(resolveAuthorizationGrantType(grantType))))
.redirectUris((uris) -> uris.addAll(redirectUris))
.postLogoutRedirectUris((uris) -> uris.addAll(postLogoutRedirectUris))
.scopes((scopes) -> scopes.addAll(clientScopes));
// @formatter:on

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -40,6 +40,7 @@ public class RegisteredClientTests {
private static final String CLIENT_ID = "client-1";
private static final String CLIENT_SECRET = "secret";
private static final Set<String> REDIRECT_URIS = Collections.singleton("https://example.com");
private static final Set<String> POST_LOGOUT_REDIRECT_URIS = Collections.singleton("https://example.com/oidc-post-logout");
private static final Set<String> SCOPES = Collections.unmodifiableSet(
Stream.of("openid", "profile", "email").collect(Collectors.toSet()));
private static final Set<ClientAuthenticationMethod> CLIENT_AUTHENTICATION_METHODS =
@@ -71,6 +72,7 @@ public class RegisteredClientTests {
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.redirectUris(redirectUris -> redirectUris.addAll(REDIRECT_URIS))
.postLogoutRedirectUris(postLogoutRedirectUris -> postLogoutRedirectUris.addAll(POST_LOGOUT_REDIRECT_URIS))
.scopes(scopes -> scopes.addAll(SCOPES))
.build();
@@ -84,6 +86,7 @@ public class RegisteredClientTests {
.isEqualTo(Collections.singleton(AuthorizationGrantType.AUTHORIZATION_CODE));
assertThat(registration.getClientAuthenticationMethods()).isEqualTo(CLIENT_AUTHENTICATION_METHODS);
assertThat(registration.getRedirectUris()).isEqualTo(REDIRECT_URIS);
assertThat(registration.getPostLogoutRedirectUris()).isEqualTo(POST_LOGOUT_REDIRECT_URIS);
assertThat(registration.getScopes()).isEqualTo(SCOPES);
}
@@ -229,6 +232,35 @@ public class RegisteredClientTests {
).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void buildWhenPostLogoutRedirectUriInvalidThenThrowIllegalArgumentException() {
assertThatThrownBy(() ->
RegisteredClient.withId(ID)
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.redirectUris(redirectUris -> redirectUris.addAll(REDIRECT_URIS))
.postLogoutRedirectUri("invalid URI")
.build()
).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void buildWhenPostLogoutRedirectUriContainsFragmentThenThrowIllegalArgumentException() {
assertThatThrownBy(() ->
RegisteredClient.withId(ID)
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.redirectUri("https://example.com")
.postLogoutRedirectUri("https://example.com/index#fragment")
.scopes(scopes -> scopes.addAll(SCOPES))
.build()
).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void buildWhenTwoAuthorizationGrantTypesAreProvidedThenBothAreRegistered() {
RegisteredClient registration = RegisteredClient.withId(ID)
@@ -345,6 +377,8 @@ public class RegisteredClientTests {
assertThat(registration.getAuthorizationGrantTypes()).isNotSameAs(updated.getAuthorizationGrantTypes());
assertThat(registration.getRedirectUris()).isEqualTo(updated.getRedirectUris());
assertThat(registration.getRedirectUris()).isNotSameAs(updated.getRedirectUris());
assertThat(registration.getPostLogoutRedirectUris()).isEqualTo(updated.getPostLogoutRedirectUris());
assertThat(registration.getPostLogoutRedirectUris()).isNotSameAs(updated.getPostLogoutRedirectUris());
assertThat(registration.getScopes()).isEqualTo(updated.getScopes());
assertThat(registration.getScopes()).isNotSameAs(updated.getScopes());
assertThat(registration.getClientSettings()).isEqualTo(updated.getClientSettings());
@@ -360,6 +394,7 @@ public class RegisteredClientTests {
String newSecret = "new-secret";
String newScope = "new-scope";
String newRedirectUri = "https://another-redirect-uri.com";
String newPostLogoutRedirectUri = "https://another-post-logout-redirect-uri.com";
RegisteredClient updated = RegisteredClient.from(registration)
.clientName(newName)
.clientSecret(newSecret)
@@ -371,6 +406,10 @@ public class RegisteredClientTests {
redirectUris.clear();
redirectUris.add(newRedirectUri);
})
.postLogoutRedirectUris(postLogoutRedirectUris -> {
postLogoutRedirectUris.clear();
postLogoutRedirectUris.add(newPostLogoutRedirectUri);
})
.build();
assertThat(registration.getClientName()).isNotEqualTo(newName);
@@ -381,6 +420,8 @@ public class RegisteredClientTests {
assertThat(updated.getScopes()).containsExactly(newScope);
assertThat(registration.getRedirectUris()).doesNotContain(newRedirectUri);
assertThat(updated.getRedirectUris()).containsExactly(newRedirectUri);
assertThat(registration.getPostLogoutRedirectUris()).doesNotContain(newPostLogoutRedirectUri);
assertThat(updated.getPostLogoutRedirectUris()).containsExactly(newPostLogoutRedirectUri);
}
@Test

View File

@@ -38,6 +38,7 @@ public class TestRegisteredClients {
.redirectUri("https://example.com/callback-1")
.redirectUri("https://example.com/callback-2")
.redirectUri("https://example.com/callback-3")
.postLogoutRedirectUri("https://example.com/oidc-post-logout")
.scope("scope1");
}
@@ -52,6 +53,7 @@ public class TestRegisteredClients {
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.redirectUri("https://example.com")
.postLogoutRedirectUri("https://example.com/oidc-post-logout")
.scope("scope1")
.scope("scope2");
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -126,6 +126,7 @@ public class OidcProviderConfigurationTests {
jsonPath("$.token_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
jsonPath("jwks_uri").value(ISSUER_URL.concat(this.authorizationServerSettings.getJwkSetEndpoint())),
jsonPath("userinfo_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())),
jsonPath("end_session_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcLogoutEndpoint())),
jsonPath("response_types_supported").value(OAuth2AuthorizationResponseType.CODE.getValue()),
jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()),

View File

@@ -47,6 +47,7 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -123,6 +124,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public class OidcTests {
private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final String DEFAULT_OIDC_LOGOUT_ENDPOINT_URI = "/connect/logout";
private static final String AUTHORITIES_CLAIM = "authorities";
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private static EmbeddedDatabase db;
@@ -216,8 +218,9 @@ public class OidcTests {
servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
// Assert user authorities was propagated as claim in ID Token
Jwt idToken = this.jwtDecoder.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
// Assert user authorities was propagated as claim in ID Token
List<String> authoritiesClaim = idToken.getClaim(AUTHORITIES_CLAIM);
Authentication principal = authorization.getAttribute(Principal.class.getName());
Set<String> userAuthorities = new HashSet<>();
@@ -225,6 +228,59 @@ public class OidcTests {
userAuthorities.add(authority.getAuthority());
}
assertThat(authoritiesClaim).containsExactlyInAnyOrderElementsOf(userAuthorities);
// Assert sid claim was added in ID Token
assertThat(idToken.<String>getClaim("sid")).isNotNull();
}
@Test
public void requestWhenLogoutRequestThenLogout() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
this.registeredClientRepository.save(registeredClient);
// Login
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient);
MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(authorizationRequestParameters)
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
MockHttpSession session = (MockHttpSession) mvcResult.getRequest().getSession();
assertThat(session.isNew()).isTrue();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
// Get ID Token
mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient, authorization))
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
registeredClient.getClientId(), registeredClient.getClientSecret()))
.session(session))
.andExpect(status().isOk())
.andReturn();
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
String idToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN);
// Logout
mvcResult = this.mvc.perform(post(DEFAULT_OIDC_LOGOUT_ENDPOINT_URI)
.param("id_token_hint", idToken)
.session(session))
.andExpect(status().is3xxRedirection())
.andReturn();
redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
assertThat(redirectedUrl).matches("/");
assertThat(session.isInvalid()).isTrue();
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -58,6 +58,7 @@ public class OidcClientRegistrationTests {
.clientSecretExpiresAt(clientSecretExpiresAt)
.clientName("client-name")
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("https://client.example.com/oidc-post-logout")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
@@ -79,6 +80,7 @@ public class OidcClientRegistrationTests {
assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt);
assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
assertThat(clientRegistration.getPostLogoutRedirectUris()).containsOnly("https://client.example.com/oidc-post-logout");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName());
assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
@@ -108,6 +110,7 @@ public class OidcClientRegistrationTests {
claims.put(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
claims.put(OidcClientMetadataClaimNames.CLIENT_NAME, "client-name");
claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, Collections.singletonList("https://client.example.com"));
claims.put(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, Collections.singletonList("https://client.example.com/oidc-post-logout"));
claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, MacAlgorithm.HS256.getName());
claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, Arrays.asList(
@@ -128,6 +131,7 @@ public class OidcClientRegistrationTests {
assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt);
assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
assertThat(clientRegistration.getPostLogoutRedirectUris()).containsOnly("https://client.example.com/oidc-post-logout");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName());
assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
@@ -261,6 +265,41 @@ public class OidcClientRegistrationTests {
assertThat(clientRegistration.getRedirectUris()).containsExactly("https://client2.example.com");
}
@Test
public void buildWhenPostLogoutRedirectUrisNotListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.minimalBuilder
.claim(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, "postLogoutRedirectUris");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("post_logout_redirect_uris must be of type List");
}
@Test
public void buildWhenPostLogoutRedirectUrisEmptyListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.minimalBuilder
.claim(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, Collections.emptyList());
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("post_logout_redirect_uris cannot be empty");
}
@Test
public void buildWhenPostLogoutRedirectUrisAddingOrRemovingThenCorrectValues() {
// @formatter:off
OidcClientRegistration clientRegistration = this.minimalBuilder
.postLogoutRedirectUri("https://client1.example.com/oidc-post-logout")
.postLogoutRedirectUris(postLogoutRedirectUris -> {
postLogoutRedirectUris.clear();
postLogoutRedirectUris.add("https://client2.example.com/oidc-post-logout");
})
.build();
// @formatter:on
assertThat(clientRegistration.getPostLogoutRedirectUris()).containsExactly("https://client2.example.com/oidc-post-logout");
}
@Test
public void buildWhenGrantTypesNotListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.minimalBuilder

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -62,6 +62,7 @@ public class OidcProviderConfigurationTests {
.userInfoEndpoint("https://example.com/issuer1/userinfo")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
.clientRegistrationEndpoint("https://example.com/issuer1/connect/register")
.endSessionEndpoint("https://example.com/issuer1/connect/logout")
.claim("a-claim", "a-value")
.build();
@@ -77,6 +78,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo"));
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
assertThat(providerConfiguration.getEndSessionEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/logout"));
assertThat(providerConfiguration.<String>getClaim("a-claim")).isEqualTo("a-value");
}
@@ -118,6 +120,7 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256"));
claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, "https://example.com/issuer1/userinfo");
claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, "https://example.com/issuer1/connect/register");
claims.put(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT, "https://example.com/issuer1/connect/logout");
claims.put("some-claim", "some-value");
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build();
@@ -134,6 +137,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo"));
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
assertThat(providerConfiguration.getEndSessionEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/logout"));
assertThat(providerConfiguration.<String>getClaim("some-claim")).isEqualTo("some-value");
}
@@ -150,6 +154,7 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256"));
claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, url("https://example.com/issuer1/userinfo"));
claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, url("https://example.com/issuer1/connect/register"));
claims.put(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT, url("https://example.com/issuer1/connect/logout"));
claims.put("some-claim", "some-value");
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build();
@@ -166,6 +171,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo"));
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
assertThat(providerConfiguration.getEndSessionEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/logout"));
assertThat(providerConfiguration.<String>getClaim("some-claim")).isEqualTo("some-value");
}
@@ -412,6 +418,16 @@ public class OidcProviderConfigurationTests {
.withMessage("clientRegistrationEndpoint must be a valid URL");
}
@Test
public void buildWhenEndSessionEndpointNotUrlThenThrowIllegalArgumentException() {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT, "not an url"));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("endSessionEndpoint must be a valid URL");
}
@Test
public void responseTypesWhenAddingOrRemovingThenCorrectValues() {
OidcProviderConfiguration configuration = this.minimalConfigurationBuilder

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -359,6 +359,78 @@ public class OidcClientRegistrationAuthenticationProviderTests {
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenInvalidPostLogoutRedirectUriThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(),
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
registeredClient, jwtAccessToken, jwt.getClaims()).build();
when(this.authorizationService.findByToken(
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
.thenReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create"));
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("invalid uri")
.build();
// @formatter:on
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo("invalid_client_metadata");
assertThat(error.getDescription()).contains(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
});
verify(this.authorizationService).findByToken(
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenPostLogoutRedirectUriContainsFragmentThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(),
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
registeredClient, jwtAccessToken, jwt.getClaims()).build();
when(this.authorizationService.findByToken(
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
.thenReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create"));
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("https://client.example.com/oidc-post-logout#fragment")
.build();
// @formatter:on
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo("invalid_client_metadata");
assertThat(error.getDescription()).contains(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
});
verify(this.authorizationService).findByToken(
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenInvalidTokenEndpointAuthenticationMethodThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
@@ -545,6 +617,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("https://client.example.com/oidc-post-logout")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
@@ -588,6 +661,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
assertThat(registeredClientResult.getClientName()).isEqualTo(clientRegistration.getClientName());
assertThat(registeredClientResult.getClientAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
assertThat(registeredClientResult.getRedirectUris()).containsExactly("https://client.example.com");
assertThat(registeredClientResult.getPostLogoutRedirectUris()).containsExactly("https://client.example.com/oidc-post-logout");
assertThat(registeredClientResult.getAuthorizationGrantTypes())
.containsExactlyInAnyOrder(AuthorizationGrantType.AUTHORIZATION_CODE, AuthorizationGrantType.CLIENT_CREDENTIALS);
assertThat(registeredClientResult.getScopes()).containsExactlyInAnyOrder("scope1", "scope2");
@@ -603,6 +677,8 @@ public class OidcClientRegistrationAuthenticationProviderTests {
assertThat(clientRegistrationResult.getClientName()).isEqualTo(registeredClientResult.getClientName());
assertThat(clientRegistrationResult.getRedirectUris())
.containsExactlyInAnyOrderElementsOf(registeredClientResult.getRedirectUris());
assertThat(clientRegistrationResult.getPostLogoutRedirectUris())
.containsExactlyInAnyOrderElementsOf(registeredClientResult.getPostLogoutRedirectUris());
List<String> grantTypes = new ArrayList<>();
registeredClientResult.getAuthorizationGrantTypes().forEach(authorizationGrantType ->

View File

@@ -0,0 +1,426 @@
/*
* Copyright 2020-2023 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.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
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.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link OidcLogoutAuthenticationProvider}.
*
* @author Joe Grandja
*/
public class OidcLogoutAuthenticationProviderTests {
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private RegisteredClientRepository registeredClientRepository;
private OAuth2AuthorizationService authorizationService;
private SessionRegistry sessionRegistry;
private AuthorizationServerSettings authorizationServerSettings;
private OidcLogoutAuthenticationProvider authenticationProvider;
@BeforeEach
public void setUp() {
this.registeredClientRepository = mock(RegisteredClientRepository.class);
this.authorizationService = mock(OAuth2AuthorizationService.class);
this.sessionRegistry = mock(SessionRegistry.class);
this.authorizationServerSettings = AuthorizationServerSettings.builder().issuer("https://provider.com").build();
TestAuthorizationServerContext authorizationServerContext =
new TestAuthorizationServerContext(this.authorizationServerSettings, null);
AuthorizationServerContextHolder.setContext(authorizationServerContext);
this.authenticationProvider = new OidcLogoutAuthenticationProvider(
this.registeredClientRepository, this.authorizationService, this.sessionRegistry);
}
@AfterEach
public void cleanup() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationProvider(null, this.authorizationService, this.sessionRegistry))
.withMessage("registeredClientRepository cannot be null");
}
@Test
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationProvider(this.registeredClientRepository, null, this.sessionRegistry))
.withMessage("authorizationService cannot be null");
}
@Test
public void constructorWhenSessionRegistryNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationProvider(this.registeredClientRepository, this.authorizationService, null))
.withMessage("sessionRegistry cannot be null");
}
@Test
public void supportsWhenTypeOidcLogoutAuthenticationTokenThenReturnTrue() {
assertThat(this.authenticationProvider.supports(OidcLogoutAuthenticationToken.class)).isTrue();
}
@Test
public void authenticateWhenIdTokenNotFoundThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", principal, "session-1", null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains("id_token_hint");
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
}
@Test
public void authenticateWhenMissingAudienceThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject(principal.getName())
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, "session-1", null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains(IdTokenClaimNames.AUD);
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenInvalidAudienceThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject(principal.getName())
.audience(Collections.singleton(registeredClient.getClientId() + "-invalid"))
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, "session-1", null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains(IdTokenClaimNames.AUD);
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
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))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, "session-1", registeredClient.getClientId() + "-invalid", null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains(OAuth2ParameterNames.CLIENT_ID);
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenInvalidPostLogoutRedirectUriThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
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))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, "session-1", registeredClient.getClientId(),
"https://example.com/callback-1-invalid", null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
assertThat(error.getDescription()).contains("post_logout_redirect_uri");
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenMissingSidThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
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))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
String sessionId = "session-1";
List<SessionInformation> sessions = Collections.singletonList(
new SessionInformation(principal.getPrincipal(), sessionId, Date.from(Instant.now())));
when(this.sessionRegistry.getAllSessions(eq(principal.getPrincipal()), eq(true)))
.thenReturn(sessions);
principal.setAuthenticated(true);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, sessionId, null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains("sid");
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenInvalidSidThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
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", "other-session")
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
String sessionId = "session-1";
List<SessionInformation> sessions = Collections.singletonList(
new SessionInformation(principal.getPrincipal(), sessionId, Date.from(Instant.now())));
when(this.sessionRegistry.getAllSessions(eq(principal.getPrincipal()), eq(true)))
.thenReturn(sessions);
principal.setAuthenticated(true);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, sessionId, null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains("sid");
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenValidIdTokenThenAuthenticated() {
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", sessionId)
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
SessionInformation sessionInformation = new SessionInformation(
principal.getPrincipal(), sessionId, Date.from(Instant.now()));
List<SessionInformation> sessions = Collections.singletonList(sessionInformation);
when(this.sessionRegistry.getAllSessions(eq(principal.getPrincipal()), eq(true)))
.thenReturn(sessions);
principal.setAuthenticated(true);
String postLogoutRedirectUri = registeredClient.getPostLogoutRedirectUris().toArray(new String[0])[0];
String state = "state";
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, sessionId, registeredClient.getClientId(), postLogoutRedirectUri, state);
OidcLogoutAuthenticationToken authenticationResult =
(OidcLogoutAuthenticationToken) this.authenticationProvider.authenticate(authentication);
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
assertThat(authenticationResult.getPrincipal()).isEqualTo(principal);
assertThat(authenticationResult.getCredentials().toString()).isEmpty();
assertThat(authenticationResult.getIdToken()).isEqualTo(idToken.getTokenValue());
assertThat(authenticationResult.getSessionId()).isEqualTo(sessionInformation.getSessionId());
assertThat(authenticationResult.getSessionInformation()).isEqualTo(sessionInformation);
assertThat(authenticationResult.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(authenticationResult.getPostLogoutRedirectUri()).isEqualTo(postLogoutRedirectUri);
assertThat(authenticationResult.getState()).isEqualTo(state);
assertThat(authenticationResult.isAuthenticated()).isTrue();
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2020-2023 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.sql.Date;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.session.SessionInformation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link OidcLogoutAuthenticationToken}.
*
* @author Joe Grandja
*/
public class OidcLogoutAuthenticationTokenTests {
private final String idToken = "id-token";
private final TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
private final String sessionId = "session-1";
private final SessionInformation sessionInformation = new SessionInformation(this.principal, "session-2", Date.from(Instant.now()));
private final String clientId = "client-1";
private final String postLogoutRedirectUri = "https://example.com/oidc-post-logout";
private final String state = "state-1";
@Test
public void constructorWhenIdTokenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
null, this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("idToken cannot be empty");
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
null, this.principal, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("idToken cannot be empty");
}
@Test
public void constructorWhenIdTokenEmptyThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
"", this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("idToken cannot be empty");
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
"", this.principal, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("idToken cannot be empty");
}
@Test
public void constructorWhenPrincipalNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
this.idToken, null, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("principal cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
this.idToken, null, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("principal cannot be null");
}
@Test
public void constructorWhenSessionIdProvidedThenCreated() {
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
this.idToken, this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state);
assertThat(authentication.getPrincipal()).isEqualTo(this.principal);
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.getIdToken()).isEqualTo(this.idToken);
assertThat(authentication.getSessionId()).isEqualTo(this.sessionId);
assertThat(authentication.getSessionInformation()).isNull();
assertThat(authentication.getClientId()).isEqualTo(this.clientId);
assertThat(authentication.getPostLogoutRedirectUri()).isEqualTo(this.postLogoutRedirectUri);
assertThat(authentication.getState()).isEqualTo(this.state);
assertThat(authentication.isAuthenticated()).isFalse();
}
@Test
public void constructorWhenSessionInformationProvidedThenCreated() {
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
this.idToken, this.principal, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state);
assertThat(authentication.getPrincipal()).isEqualTo(this.principal);
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.getIdToken()).isEqualTo(this.idToken);
assertThat(authentication.getSessionId()).isEqualTo(this.sessionInformation.getSessionId());
assertThat(authentication.getSessionInformation()).isEqualTo(this.sessionInformation);
assertThat(authentication.getClientId()).isEqualTo(this.clientId);
assertThat(authentication.getPostLogoutRedirectUri()).isEqualTo(this.postLogoutRedirectUri);
assertThat(authentication.getState()).isEqualTo(this.state);
assertThat(authentication.isAuthenticated()).isTrue();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -99,6 +99,9 @@ public class OidcClientRegistrationHttpMessageConverterTests {
+" \"redirect_uris\": [\n"
+ " \"https://client.example.com\"\n"
+ " ],\n"
+" \"post_logout_redirect_uris\": [\n"
+ " \"https://client.example.com/oidc-post-logout\"\n"
+ " ],\n"
+" \"token_endpoint_auth_method\": \"client_secret_jwt\",\n"
+" \"token_endpoint_auth_signing_alg\": \"HS256\",\n"
+" \"grant_types\": [\n"
@@ -125,6 +128,7 @@ public class OidcClientRegistrationHttpMessageConverterTests {
assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(Instant.ofEpochSecond(1607637467L));
assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
assertThat(clientRegistration.getPostLogoutRedirectUris()).containsOnly("https://client.example.com/oidc-post-logout");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName());
assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
@@ -183,6 +187,7 @@ public class OidcClientRegistrationHttpMessageConverterTests {
.clientSecretExpiresAt(Instant.ofEpochSecond(1607637467))
.clientName("client-name")
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("https://client.example.com/oidc-post-logout")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
@@ -208,6 +213,7 @@ public class OidcClientRegistrationHttpMessageConverterTests {
assertThat(clientRegistrationResponse).contains("\"client_secret_expires_at\":1607637467");
assertThat(clientRegistrationResponse).contains("\"client_name\":\"client-name\"");
assertThat(clientRegistrationResponse).contains("\"redirect_uris\":[\"https://client.example.com\"]");
assertThat(clientRegistrationResponse).contains("\"post_logout_redirect_uris\":[\"https://client.example.com/oidc-post-logout\"]");
assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_method\":\"client_secret_jwt\"");
assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_signing_alg\":\"HS256\"");
assertThat(clientRegistrationResponse).contains("\"grant_types\":[\"authorization_code\",\"client_credentials\"]");

View File

@@ -0,0 +1,363 @@
/*
* Copyright 2020-2023 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.web;
import java.time.Instant;
import java.util.Date;
import java.util.function.Consumer;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
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.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* Tests for {@link OidcLogoutEndpointFilter}.
*
* @author Joe Grandja
*/
public class OidcLogoutEndpointFilterTests {
private static final String DEFAULT_OIDC_LOGOUT_ENDPOINT_URI = "/connect/logout";
private AuthenticationManager authenticationManager;
private OidcLogoutEndpointFilter filter;
private TestingAuthenticationToken principal;
@BeforeEach
public void setUp() {
this.authenticationManager = mock(AuthenticationManager.class);
this.filter = new OidcLogoutEndpointFilter(this.authenticationManager);
this.principal = new TestingAuthenticationToken("principal", "credentials");
this.principal.setAuthenticated(true);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
}
@AfterEach
public void cleanup() {
SecurityContextHolder.clearContext();
}
@Test
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcLogoutEndpointFilter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationManager cannot be null");
}
@Test
public void constructorWhenLogoutEndpointUriNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcLogoutEndpointFilter(this.authenticationManager, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("logoutEndpointUri cannot be empty");
}
@Test
public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationConverter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationConverter cannot be null");
}
@Test
public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationSuccessHandler cannot be null");
}
@Test
public void setAuthenticationFailureHandlerWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationFailureHandler(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationFailureHandler cannot be null");
}
@Test
public void doFilterWhenNotLogoutRequestThenNotProcessed() throws Exception {
String requestUri = "/path";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenLogoutRequestMissingIdTokenHintThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
"id_token_hint",
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.removeParameter("id_token_hint"));
}
@Test
public void doFilterWhenLogoutRequestMultipleIdTokenHintThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
"id_token_hint",
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.addParameter("id_token_hint", "id-token-2"));
}
@Test
public void doFilterWhenLogoutRequestMultipleClientIdThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
OAuth2ParameterNames.CLIENT_ID,
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2"));
}
@Test
public void doFilterWhenLogoutRequestMultiplePostLogoutRedirectUriThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
"post_logout_redirect_uri",
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.addParameter("post_logout_redirect_uri", "https://example.com/callback-4"));
}
@Test
public void doFilterWhenLogoutRequestMultipleStateThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
OAuth2ParameterNames.STATE,
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.addParameter(OAuth2ParameterNames.STATE, "state-2"));
}
private void doFilterWhenRequestInvalidParameterThenError(MockHttpServletRequest request,
String parameterName, String errorCode, Consumer<MockHttpServletRequest> requestConsumer) throws Exception {
requestConsumer.accept(request);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
assertThat(response.getErrorMessage()).isEqualTo("[" + errorCode + "] OpenID Connect 1.0 Logout Request Parameter: " + parameterName);
}
@Test
public void doFilterWhenLogoutRequestAuthenticationExceptionThenErrorResponse() throws Exception {
OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri");
when(this.authenticationManager.authenticate(any()))
.thenThrow(new OAuth2AuthenticationException(error));
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
assertThat(response.getErrorMessage()).isEqualTo(error.toString());
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.principal);
}
@Test
public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception {
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", this.principal, (SessionInformation) null, null, null, null);
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
when(authenticationConverter.convert(any())).thenReturn(authentication);
this.filter.setAuthenticationConverter(authenticationConverter);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authentication);
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(authenticationConverter).convert(any());
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
}
@Test
public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", this.principal, (SessionInformation) null, null, null, null);
AuthenticationSuccessHandler authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
this.filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authentication);
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), same(authentication));
verifyNoInteractions(filterChain);
}
@Test
public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception {
AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
this.filter.setAuthenticationFailureHandler(authenticationFailureHandler);
when(this.authenticationManager.authenticate(any()))
.thenThrow(new AuthenticationServiceException("AuthenticationServiceException"));
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
ArgumentCaptor<AuthenticationException> authenticationExceptionCaptor = ArgumentCaptor.forClass(AuthenticationException.class);
verify(this.authenticationManager).authenticate(any());
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), authenticationExceptionCaptor.capture());
verifyNoInteractions(filterChain);
assertThat(authenticationExceptionCaptor.getValue())
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
assertThat(error.getDescription()).contains("AuthenticationServiceException");
});
}
@Test
public void doFilterWhenLogoutRequestAuthenticatedThenLogout() throws Exception {
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpSession session = (MockHttpSession) request.getSession(true);
SessionInformation sessionInformation = new SessionInformation(
this.principal, session.getId(), Date.from(Instant.now()));
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", this.principal, sessionInformation, null, null, null);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authentication);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).isEqualTo("/");
assertThat(session.isInvalid()).isTrue();
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
}
@Test
public void doFilterWhenLogoutRequestAuthenticatedWithPostLogoutRedirectUriThenPostLogoutRedirect() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
MockHttpServletRequest request = createLogoutRequest(registeredClient);
MockHttpSession session = (MockHttpSession) request.getSession(true);
SessionInformation sessionInformation = new SessionInformation(
this.principal, session.getId(), Date.from(Instant.now()));
String postLogoutRedirectUri = registeredClient.getPostLogoutRedirectUris().iterator().next();
String state = "state-1";
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", this.principal, sessionInformation,
registeredClient.getClientId(), postLogoutRedirectUri, state);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authentication);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).isEqualTo(postLogoutRedirectUri + "?state=" + state);
assertThat(session.isInvalid()).isTrue();
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
}
private static MockHttpServletRequest createLogoutRequest(RegisteredClient registeredClient) {
String requestUri = DEFAULT_OIDC_LOGOUT_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.addParameter("id_token_hint", "id-token");
request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
request.addParameter("post_logout_redirect_uri", registeredClient.getPostLogoutRedirectUris().iterator().next());
request.addParameter(OAuth2ParameterNames.STATE, "state");
return request;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -92,6 +92,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
String tokenEndpoint = "/oauth2/v1/token";
String jwkSetEndpoint = "/oauth2/v1/jwks";
String userInfoEndpoint = "/userinfo";
String logoutEndpoint = "/connect/logout";
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
@@ -101,6 +102,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
.tokenEndpoint(tokenEndpoint)
.jwkSetEndpoint(jwkSetEndpoint)
.oidcUserInfoEndpoint(userInfoEndpoint)
.oidcLogoutEndpoint(logoutEndpoint)
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
.build();
@@ -132,6 +134,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]");
assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]");
assertThat(providerConfigurationResponse).contains("\"userinfo_endpoint\":\"https://example.com/issuer1/userinfo\"");
assertThat(providerConfigurationResponse).contains("\"end_session_endpoint\":\"https://example.com/issuer1/connect/logout\"");
assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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 static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* Tests for {@link AuthorizationServerSettings}.
*
* @author Daniel Garnier-Moiroux
* @author Joe Grandja
*/
public class AuthorizationServerSettingsTests {
@@ -39,6 +40,7 @@ public class AuthorizationServerSettingsTests {
assertThat(authorizationServerSettings.getTokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
assertThat(authorizationServerSettings.getOidcClientRegistrationEndpoint()).isEqualTo("/connect/register");
assertThat(authorizationServerSettings.getOidcUserInfoEndpoint()).isEqualTo("/userinfo");
assertThat(authorizationServerSettings.getOidcLogoutEndpoint()).isEqualTo("/connect/logout");
}
@Test
@@ -50,6 +52,7 @@ public class AuthorizationServerSettingsTests {
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
String oidcClientRegistrationEndpoint = "/connect/v1/register";
String oidcUserInfoEndpoint = "/connect/v1/userinfo";
String oidcLogoutEndpoint = "/connect/v1/logout";
String issuer = "https://example.com:9000";
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
@@ -62,6 +65,7 @@ public class AuthorizationServerSettingsTests {
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.oidcClientRegistrationEndpoint(oidcClientRegistrationEndpoint)
.oidcUserInfoEndpoint(oidcUserInfoEndpoint)
.oidcLogoutEndpoint(oidcLogoutEndpoint)
.build();
assertThat(authorizationServerSettings.getIssuer()).isEqualTo(issuer);
@@ -72,6 +76,7 @@ public class AuthorizationServerSettingsTests {
assertThat(authorizationServerSettings.getTokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint);
assertThat(authorizationServerSettings.getOidcClientRegistrationEndpoint()).isEqualTo(oidcClientRegistrationEndpoint);
assertThat(authorizationServerSettings.getOidcUserInfoEndpoint()).isEqualTo(oidcUserInfoEndpoint);
assertThat(authorizationServerSettings.getOidcLogoutEndpoint()).isEqualTo(oidcLogoutEndpoint);
}
@Test
@@ -81,7 +86,7 @@ public class AuthorizationServerSettingsTests {
.settings(settings -> settings.put("name2", "value2"))
.build();
assertThat(authorizationServerSettings.getSettings()).hasSize(9);
assertThat(authorizationServerSettings.getSettings()).hasSize(10);
assertThat(authorizationServerSettings.<String>getSetting("name1")).isEqualTo("value1");
assertThat(authorizationServerSettings.<String>getSetting("name2")).isEqualTo("value2");
}
@@ -142,4 +147,11 @@ public class AuthorizationServerSettingsTests {
.withMessage("value cannot be null");
}
@Test
public void oidcLogoutEndpointWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> AuthorizationServerSettings.builder().oidcLogoutEndpoint(null))
.withMessage("value cannot be null");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.token;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -27,6 +28,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
@@ -46,7 +48,6 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
@@ -67,7 +68,7 @@ public class JwtGeneratorTests {
private JwtEncoder jwtEncoder;
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
private JwtGenerator jwtGenerator;
private AuthorizationServerContext authorizationServerContext;
private TestAuthorizationServerContext authorizationServerContext;
@BeforeEach
public void setUp() {
@@ -168,16 +169,21 @@ public class JwtGeneratorTests {
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, authorizationRequest.getRedirectUri(), null);
Authentication principal = authorization.getAttribute(Principal.class.getName());
SessionInformation sessionInformation = new SessionInformation(
principal.getPrincipal(), "session1", Date.from(Instant.now().minus(2, ChronoUnit.HOURS)));
// @formatter:off
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(authorization.getAttribute(Principal.class.getName()))
.principal(principal)
.authorizationServerContext(this.authorizationServerContext)
.authorization(authorization)
.authorizedScopes(authorization.getAuthorizedScopes())
.tokenType(ID_TOKEN_TOKEN_TYPE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrant(authentication)
.put(SessionInformation.class, sessionInformation)
.build();
// @formatter:on
@@ -238,6 +244,10 @@ public class JwtGeneratorTests {
OAuth2AuthorizationRequest.class.getName());
String nonce = (String) authorizationRequest.getAdditionalParameters().get(OidcParameterNames.NONCE);
assertThat(jwtClaimsSet.<String>getClaim(IdTokenClaimNames.NONCE)).isEqualTo(nonce);
SessionInformation sessionInformation = tokenContext.get(SessionInformation.class);
assertThat(jwtClaimsSet.<String>getClaim("sid")).isEqualTo(sessionInformation.getSessionId());
assertThat(jwtClaimsSet.<Date>getClaim(IdTokenClaimNames.AUTH_TIME)).isEqualTo(sessionInformation.getLastRequest());
}
}

View File

@@ -58,6 +58,7 @@ import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -151,6 +152,13 @@ public class OAuth2AuthorizationEndpointFilterTests {
.hasMessage("authenticationFailureHandler cannot be null");
}
@Test
public void setSessionAuthenticationStrategyWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setSessionAuthenticationStrategy(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("sessionAuthenticationStrategy cannot be null");
}
@Test
public void doFilterWhenNotAuthorizationRequestThenNotProcessed() throws Exception {
String requestUri = "/path";
@@ -383,6 +391,31 @@ public class OAuth2AuthorizationEndpointFilterTests {
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), same(authenticationException));
}
@Test
public void doFilterWhenCustomSessionAuthenticationStrategyThenUsed() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
new OAuth2AuthorizationCodeRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), principal, this.authorizationCode,
registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes());
authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authorizationCodeRequestAuthenticationResult);
SessionAuthenticationStrategy sessionAuthenticationStrategy = mock(SessionAuthenticationStrategy.class);
this.filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
verify(sessionAuthenticationStrategy).onAuthentication(same(authorizationCodeRequestAuthenticationResult), any(), any());
}
@Test
public void doFilterWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();

View File

@@ -8,6 +8,7 @@ CREATE TABLE oauth2RegisteredClient (
clientAuthenticationMethods varchar(1000) NOT NULL,
authorizationGrantTypes varchar(1000) NOT NULL,
redirectUris varchar(1000) DEFAULT NULL,
postLogoutRedirectUris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
clientSettings varchar(2000) NOT NULL,
tokenSettings varchar(2000) NOT NULL,

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -94,6 +94,7 @@ public class AuthorizationServerConfig {
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.postLogoutRedirectUri("http://127.0.0.1:8080/index")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -19,11 +19,14 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import static org.springframework.security.config.Customizer.withDefaults;
@@ -58,4 +61,14 @@ public class DefaultSecurityConfig {
}
// @formatter:on
@Bean
SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -88,6 +88,7 @@ public class AuthorizationServerConfig {
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.postLogoutRedirectUri("http://127.0.0.1:8080/index")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -19,11 +19,14 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import static org.springframework.security.config.Customizer.withDefaults;
@@ -59,4 +62,14 @@ public class DefaultSecurityConfig {
}
// @formatter:on
@Bean
SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -90,6 +90,7 @@ public class AuthorizationServerConfig {
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.postLogoutRedirectUri("http://127.0.0.1:8080/index")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -23,11 +23,14 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;
/**
* @author Steve Riesenberg
@@ -66,4 +69,14 @@ public class DefaultSecurityConfig {
}
// @formatter:on
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -15,12 +15,16 @@
*/
package sample.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import static org.springframework.security.config.Customizer.withDefaults;
@@ -32,6 +36,9 @@ import static org.springframework.security.config.Customizer.withDefaults;
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/webjars/**");
@@ -46,9 +53,22 @@ public class SecurityConfig {
)
.oauth2Login(oauth2Login ->
oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc"))
.oauth2Client(withDefaults());
.oauth2Client(withDefaults())
.logout(logout ->
logout.logoutSuccessHandler(oidcLogoutSuccessHandler()));
return http.build();
}
// @formatter:on
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
// Set the location that the End-User's User Agent will be redirected to
// after the logout has been performed at the Provider
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/index");
return oidcLogoutSuccessHandler;
}
}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<title>Spring Security OAuth 2.0 Sample</title>
<meta charset="utf-8" />
@@ -10,10 +10,11 @@
<body>
<div th:fragment="header">
<nav class="navbar navbar-default">
<div class="container">
<div class="container-fluid">
<div class="navbar-collapse collapse" id="navbar">
</div>
<div class="container-fluid">
<div class="collapse navbar-collapse">
<form class="navbar-form navbar-right" th:action="@{/logout}" method="post">
<button type="submit" class="btn btn-default navbar-btn">Logout</button>
</form>
</div>
</div>
</nav>