Add OpenID Connect 1.0 Logout Endpoint
Closes gh-266
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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())));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)}.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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())));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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\"]");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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\"]");
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user