diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index c9d9de0c..cb8fb902 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -282,7 +282,7 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth * 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 + * @since 1.1 */ public void setSessionRegistry(SessionRegistry sessionRegistry) { Assert.notNull(sessionRegistry, "sessionRegistry cannot be null"); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/RegisteredClient.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/RegisteredClient.java index 3b7bcd09..d03afac0 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/RegisteredClient.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/client/RegisteredClient.java @@ -152,7 +152,7 @@ public class RegisteredClient implements Serializable { * 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 + * @since 1.1 */ public Set getPostLogoutRedirectUris() { return this.postLogoutRedirectUris; @@ -447,7 +447,7 @@ public class RegisteredClient implements Serializable { * * @param postLogoutRedirectUri the post logout redirect URI * @return the {@link Builder} - * @since 1.1.0 + * @since 1.1 */ public Builder postLogoutRedirectUri(String postLogoutRedirectUri) { this.postLogoutRedirectUris.add(postLogoutRedirectUri); @@ -460,7 +460,7 @@ public class RegisteredClient implements Serializable { * * @param postLogoutRedirectUrisConsumer a {@link Consumer} of the post logout redirect URI(s) * @return the {@link Builder} - * @since 1.1.0 + * @since 1.1 */ public Builder postLogoutRedirectUris(Consumer> postLogoutRedirectUrisConsumer) { postLogoutRedirectUrisConsumer.accept(this.postLogoutRedirectUris); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java index dc6d204e..ab8cdb4b 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java @@ -23,14 +23,19 @@ import java.util.Map; import com.nimbusds.jose.jwk.source.JWKSource; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.GenericApplicationListenerAdapter; +import org.springframework.context.event.SmartApplicationListener; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; 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.context.DelegatingApplicationListener; import org.springframework.security.core.Authentication; import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2Token; @@ -270,7 +275,8 @@ public final class OAuth2AuthorizationServerConfigurer if (isOidcEnabled()) { // Add OpenID Connect session tracking capabilities. - SessionRegistry sessionRegistry = OAuth2ConfigurerUtils.getSessionRegistry(httpSecurity); + initSessionRegistry(httpSecurity); + SessionRegistry sessionRegistry = httpSecurity.getSharedObject(SessionRegistry.class); OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer = getConfigurer(OAuth2AuthorizationEndpointConfigurer.class); authorizationEndpointConfigurer.setSessionAuthenticationStrategy((authentication, request, response) -> { @@ -388,4 +394,23 @@ public final class OAuth2AuthorizationServerConfigurer } } + private static void initSessionRegistry(HttpSecurity httpSecurity) { + SessionRegistry sessionRegistry = OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, SessionRegistry.class); + if (sessionRegistry == null) { + sessionRegistry = new SessionRegistryImpl(); + registerDelegateApplicationListener(httpSecurity, (SessionRegistryImpl) sessionRegistry); + } + httpSecurity.setSharedObject(SessionRegistry.class, sessionRegistry); + } + + private static void registerDelegateApplicationListener(HttpSecurity httpSecurity, ApplicationListener delegate) { + DelegatingApplicationListener delegatingApplicationListener = + OAuth2ConfigurerUtils.getOptionalBean(httpSecurity, DelegatingApplicationListener.class); + if (delegatingApplicationListener == null) { + return; + } + SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate); + delegatingApplicationListener.addListener(smartListener); + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java index 50f0282e..9d7ab7ad 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java @@ -24,14 +24,8 @@ 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; @@ -186,28 +180,6 @@ 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 getBean(HttpSecurity httpSecurity, Class type) { return httpSecurity.getSharedObject(ApplicationContext.class).getBean(type); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java index 7229ae33..61e089f2 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java @@ -220,11 +220,13 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity); OAuth2TokenGenerator tokenGenerator = OAuth2ConfigurerUtils.getTokenGenerator(httpSecurity); - SessionRegistry sessionRegistry = OAuth2ConfigurerUtils.getSessionRegistry(httpSecurity); OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(authorizationService, tokenGenerator); - authorizationCodeAuthenticationProvider.setSessionRegistry(sessionRegistry); + SessionRegistry sessionRegistry = httpSecurity.getSharedObject(SessionRegistry.class); + if (sessionRegistry != null) { + authorizationCodeAuthenticationProvider.setSessionRegistry(sessionRegistry); + } authenticationProviders.add(authorizationCodeAuthenticationProvider); OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider = diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java index f43bab9d..d1abe967 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java @@ -72,7 +72,7 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer { * * @param logoutEndpointCustomizer the {@link Customizer} providing access to the {@link OidcLogoutEndpointConfigurer} * @return the {@link OidcConfigurer} for further configuration - * @since 1.1.0 + * @since 1.1 */ public OidcConfigurer logoutEndpoint(Customizer logoutEndpointCustomizer) { logoutEndpointCustomizer.customize(getConfigurer(OidcLogoutEndpointConfigurer.class)); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java index 92580b6e..04b356ff 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcLogoutEndpointConfigurer.java @@ -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.server.authorization.oidc.authentication.OidcLogoutAuthenticationProvider; @@ -47,7 +48,7 @@ import org.springframework.util.Assert; * Configurer for OpenID Connect 1.0 RP-Initiated Logout Endpoint. * * @author Joe Grandja - * @since 1.1.0 + * @since 1.1 * @see OidcConfigurer#logoutEndpoint * @see OidcLogoutEndpointFilter */ @@ -210,7 +211,7 @@ public final class OidcLogoutEndpointConfigurer extends AbstractOAuth2Configurer new OidcLogoutAuthenticationProvider( OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity), OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity), - OAuth2ConfigurerUtils.getSessionRegistry(httpSecurity)); + httpSecurity.getSharedObject(SessionRegistry.class)); authenticationProviders.add(oidcLogoutAuthenticationProvider); return authenticationProviders; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimAccessor.java index 8d36b9c3..09a78c72 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimAccessor.java @@ -101,7 +101,7 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor { * 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 + * @since 1.1 */ default List getPostLogoutRedirectUris() { return getClaimAsStringList(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimNames.java index e8d75537..d39990e5 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimNames.java @@ -66,7 +66,7 @@ public final class OidcClientMetadataClaimNames { * {@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 + * @since 1.1 */ public static final String POST_LOGOUT_REDIRECT_URIS = "post_logout_redirect_uris"; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistration.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistration.java index 036415ff..07490727 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistration.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistration.java @@ -176,7 +176,7 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce * * @param postLogoutRedirectUri the post logout redirection {@code URI} used by the Client * @return the {@link Builder} for further configuration - * @since 1.1.0 + * @since 1.1 */ public Builder postLogoutRedirectUri(String postLogoutRedirectUri) { addClaimToClaimList(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, postLogoutRedirectUri); @@ -189,7 +189,7 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce * * @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 + * @since 1.1 */ public Builder postLogoutRedirectUris(Consumer> postLogoutRedirectUrisConsumer) { acceptClaimValues(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, postLogoutRedirectUrisConsumer); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfiguration.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfiguration.java index e2b8567d..6309c4f1 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfiguration.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfiguration.java @@ -136,7 +136,7 @@ public final class OidcProviderConfiguration extends AbstractOAuth2Authorization * * @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 + * @since 1.1 */ public Builder endSessionEndpoint(String endSessionEndpoint) { return claim(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT, endSessionEndpoint); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimAccessor.java index d67d1a04..4afb1aaa 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimAccessor.java @@ -72,7 +72,7 @@ public interface OidcProviderMetadataClaimAccessor extends OAuth2AuthorizationSe * 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 + * @since 1.1 */ default URL getEndSessionEndpoint() { return getClaimAsURL(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimNames.java index b4bd4471..00e5d6ca 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimNames.java @@ -49,7 +49,7 @@ public final class OidcProviderMetadataClaimNames extends OAuth2AuthorizationSer /** * {@code end_session_endpoint} - the {@code URL} of the OpenID Connect 1.0 End Session Endpoint - * @since 1.1.0 + * @since 1.1 */ public static final String END_SESSION_ENDPOINT = "end_session_endpoint"; diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProvider.java index 7de465dd..a40c7b07 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProvider.java @@ -46,7 +46,7 @@ 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 + * @since 1.1 * @see RegisteredClientRepository * @see OAuth2AuthorizationService * @see SessionRegistry @@ -83,7 +83,7 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro (OidcLogoutAuthenticationToken) authentication; OAuth2Authorization authorization = this.authorizationService.findByToken( - oidcLogoutAuthentication.getIdToken(), ID_TOKEN_TOKEN_TYPE); + oidcLogoutAuthentication.getIdTokenHint(), ID_TOKEN_TOKEN_TYPE); if (authorization == null) { throwError(OAuth2ErrorCodes.INVALID_TOKEN, "id_token_hint"); } @@ -120,18 +120,24 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro this.logger.trace("Validated logout request parameters"); } - // Validate user session - SessionInformation sessionInformation = null; + // Validate user identity 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 (isPrincipalAuthenticated(userPrincipal)) { + if (!StringUtils.hasText(idToken.getSubject()) || + !idToken.getSubject().equals(userPrincipal.getName())) { + throwError(OAuth2ErrorCodes.INVALID_TOKEN, IdTokenClaimNames.SUB); + } + + // Check for active session + if (StringUtils.hasText(oidcLogoutAuthentication.getSessionId())) { + SessionInformation 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"); + } } } } @@ -140,8 +146,8 @@ public final class OidcLogoutAuthenticationProvider implements AuthenticationPro this.logger.trace("Authenticated logout request"); } - return new OidcLogoutAuthenticationToken(oidcLogoutAuthentication.getIdToken(), userPrincipal, - sessionInformation, oidcLogoutAuthentication.getClientId(), + return new OidcLogoutAuthenticationToken(idToken, userPrincipal, + oidcLogoutAuthentication.getSessionId(), oidcLogoutAuthentication.getClientId(), oidcLogoutAuthentication.getPostLogoutRedirectUri(), oidcLogoutAuthentication.getState()); } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationToken.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationToken.java index a9cc28c0..5e3d0698 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationToken.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationToken.java @@ -20,7 +20,7 @@ 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.core.oidc.OidcIdToken; import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion; import org.springframework.util.Assert; @@ -28,16 +28,16 @@ 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 + * @since 1.1 * @see AbstractAuthenticationToken * @see OidcLogoutAuthenticationProvider */ public class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; - private final String idToken; + private final String idTokenHint; + private final OidcIdToken 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; @@ -45,22 +45,22 @@ public class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken { /** * 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 idTokenHint 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 sessionId the End-User's current authenticated session identifier with the Provider * @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, + public OidcLogoutAuthenticationToken(String idTokenHint, 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.hasText(idTokenHint, "idTokenHint cannot be empty"); Assert.notNull(principal, "principal cannot be null"); - this.idToken = idToken; + this.idTokenHint = idTokenHint; + this.idToken = null; this.principal = principal; this.sessionId = sessionId; - this.sessionInformation = null; this.clientId = clientId; this.postLogoutRedirectUri = postLogoutRedirectUri; this.state = state; @@ -70,22 +70,22 @@ public class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken { /** * 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 idToken the ID Token previously issued by the Provider to 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 sessionId the End-User's current authenticated session identifier with the Provider * @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, + public OidcLogoutAuthenticationToken(OidcIdToken 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(idToken, "idToken cannot be null"); Assert.notNull(principal, "principal cannot be null"); + this.idTokenHint = idToken.getTokenValue(); this.idToken = idToken; this.principal = principal; - this.sessionId = sessionInformation != null ? sessionInformation.getSessionId() : null; - this.sessionInformation = sessionInformation; + this.sessionId = sessionId; this.clientId = clientId; this.postLogoutRedirectUri = postLogoutRedirectUri; this.state = state; @@ -95,7 +95,7 @@ public class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken { /** * Returns the authenticated principal representing the End-User. * - * @return the authenticated principal + * @return the authenticated principal representing the End-User */ @Override public Object getPrincipal() { @@ -113,30 +113,30 @@ public class OidcLogoutAuthenticationToken extends AbstractAuthenticationToken { * * @return the ID Token previously issued by the Provider to the Client */ - public String getIdToken() { + public String getIdTokenHint() { + return this.idTokenHint; + } + + /** + * Returns the ID Token previously issued by the Provider to the Client. + * + * @return the ID Token previously issued by the Provider to the Client + */ + @Nullable + public OidcIdToken getIdToken() { return this.idToken; } /** - * Returns the End-User's current authenticated session identifier with the Client. + * Returns the End-User's current authenticated session identifier with the Provider. * - * @return the End-User's current authenticated session identifier + * @return the End-User's current authenticated session identifier with the Provider */ @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. * diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcLogoutEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcLogoutEndpointFilter.java index 0292d617..b0cc3eea 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcLogoutEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcLogoutEndpointFilter.java @@ -26,6 +26,7 @@ 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.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -58,7 +59,7 @@ 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 + * @since 1.1 * @see OidcLogoutAuthenticationConverter * @see OidcLogoutAuthenticationProvider * @see 2. RP-Initiated Logout @@ -182,7 +183,7 @@ public final class OidcLogoutEndpointFilter extends OncePerRequestFilter { OidcLogoutAuthenticationToken oidcLogoutAuthentication = (OidcLogoutAuthenticationToken) authentication; // Check for active user session - if (oidcLogoutAuthentication.getSessionInformation() != null) { + if (isSessionActive(oidcLogoutAuthentication)) { // Perform logout this.logoutHandler.logout(request, response, (Authentication) oidcLogoutAuthentication.getPrincipal()); @@ -215,4 +216,15 @@ public final class OidcLogoutEndpointFilter extends OncePerRequestFilter { response.sendError(HttpStatus.BAD_REQUEST.value(), error.toString()); } + private static boolean isSessionActive(OidcLogoutAuthenticationToken oidcLogoutAuthentication) { + return isPrincipalAuthenticated((Authentication) oidcLogoutAuthentication.getPrincipal()) && + StringUtils.hasText(oidcLogoutAuthentication.getSessionId()); + } + + private static boolean isPrincipalAuthenticated(Authentication principal) { + return principal != null && + !AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) && + principal.isAuthenticated(); + } + } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/authentication/OidcLogoutAuthenticationConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/authentication/OidcLogoutAuthenticationConverter.java index 481f8a5a..530c2f06 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/authentication/OidcLogoutAuthenticationConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/authentication/OidcLogoutAuthenticationConverter.java @@ -40,7 +40,7 @@ import org.springframework.util.StringUtils; * and then converts to an {@link OidcLogoutAuthenticationToken} used for authenticating the request. * * @author Joe Grandja - * @since 1.1.0 + * @since 1.1 * @see AuthenticationConverter * @see OidcLogoutAuthenticationToken * @see OidcLogoutEndpointFilter diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java index 57af7f7e..4698da24 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java @@ -130,7 +130,7 @@ public final class AuthorizationServerSettings extends AbstractSettings { * 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 + * @since 1.1 */ public String getOidcLogoutEndpoint() { return getSetting(ConfigurationSettingNames.AuthorizationServer.OIDC_LOGOUT_ENDPOINT); @@ -282,7 +282,7 @@ public final class AuthorizationServerSettings extends AbstractSettings { * * @param oidcLogoutEndpoint the OpenID Connect 1.0 Logout endpoint * @return the {@link Builder} for further configuration - * @since 1.1.0 + * @since 1.1 */ public Builder oidcLogoutEndpoint(String oidcLogoutEndpoint) { return setting(ConfigurationSettingNames.AuthorizationServer.OIDC_LOGOUT_ENDPOINT, oidcLogoutEndpoint); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java index 0e970ddd..c51e545a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java @@ -128,7 +128,7 @@ public final class ConfigurationSettingNames { /** * Set the OpenID Connect 1.0 Logout endpoint. - * @since 1.1.0 + * @since 1.1 */ public static final String OIDC_LOGOUT_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE.concat("oidc-logout-endpoint"); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java index d32d6d16..cacb855c 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java @@ -249,7 +249,7 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte * 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 + * @since 1.1 */ public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) { Assert.notNull(sessionAuthenticationStrategy, "sessionAuthenticationStrategy cannot be null"); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java index 80e9f4a5..71e56de4 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java @@ -54,6 +54,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -131,6 +133,7 @@ public class OidcTests { private static JWKSource jwkSource; private static HttpMessageConverter accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); + private static SessionRegistry sessionRegistry; public final SpringTestContext spring = new SpringTestContext(); @@ -163,6 +166,7 @@ public class OidcTests { .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql") .addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql") .build(); + sessionRegistry = spy(new SessionRegistryImpl()); } @AfterEach @@ -187,8 +191,8 @@ public class OidcTests { MultiValueMap authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient); MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI) - .params(authorizationRequestParameters) - .with(user("user").roles("A", "B"))) + .params(authorizationRequestParameters) + .with(user("user").roles("A", "B"))) .andExpect(status().is3xxRedirection()) .andReturn(); String redirectedUrl = mvcResult.getResponse().getRedirectedUrl(); @@ -199,9 +203,9 @@ public class OidcTests { OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE); mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) - .params(getTokenRequestParameters(registeredClient, authorization)) - .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( - registeredClient.getClientId(), registeredClient.getClientSecret()))) + .params(getTokenRequestParameters(registeredClient, authorization)) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient.getClientId(), registeredClient.getClientSecret()))) .andExpect(status().isOk()) .andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store"))) .andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache"))) @@ -259,8 +263,7 @@ public class OidcTests { mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) .params(getTokenRequestParameters(registeredClient, authorization)) .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( - registeredClient.getClientId(), registeredClient.getClientSecret())) - .session(session)) + registeredClient.getClientId(), registeredClient.getClientSecret()))) .andExpect(status().isOk()) .andReturn(); @@ -283,6 +286,85 @@ public class OidcTests { assertThat(session.isInvalid()).isTrue(); } + @Test + public void requestWhenLogoutRequestWithOtherUsersIdTokenThenNotLogout() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + // Login user1 + RegisteredClient registeredClient1 = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); + this.registeredClientRepository.save(registeredClient1); + + MultiValueMap authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient1); + MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI) + .params(authorizationRequestParameters) + .with(user("user1"))) + .andExpect(status().is3xxRedirection()) + .andReturn(); + + MockHttpSession user1Session = (MockHttpSession) mvcResult.getRequest().getSession(); + assertThat(user1Session.isNew()).isTrue(); + + String redirectedUrl = mvcResult.getResponse().getRedirectedUrl(); + String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code"); + OAuth2Authorization user1Authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE); + + mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + .params(getTokenRequestParameters(registeredClient1, user1Authorization)) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient1.getClientId(), registeredClient1.getClientSecret()))) + .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 user1IdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN); + + // Login user2 + RegisteredClient registeredClient2 = TestRegisteredClients.registeredClient2().scope(OidcScopes.OPENID).build(); + this.registeredClientRepository.save(registeredClient2); + + authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient2); + mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI) + .params(authorizationRequestParameters) + .with(user("user2"))) + .andExpect(status().is3xxRedirection()) + .andReturn(); + + MockHttpSession user2Session = (MockHttpSession) mvcResult.getRequest().getSession(); + assertThat(user2Session.isNew()).isTrue(); + + redirectedUrl = mvcResult.getResponse().getRedirectedUrl(); + authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code"); + OAuth2Authorization user2Authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE); + + mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI) + .params(getTokenRequestParameters(registeredClient2, user2Authorization)) + .header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth( + registeredClient2.getClientId(), registeredClient2.getClientSecret()))) + .andExpect(status().isOk()) + .andReturn(); + + servletResponse = mvcResult.getResponse(); + httpResponse = new MockClientHttpResponse( + servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus())); + accessTokenResponse = accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse); + + String user2IdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN); + + // Attempt to log out user1 using user2's ID Token + mvcResult = this.mvc.perform(post(DEFAULT_OIDC_LOGOUT_ENDPOINT_URI) + .param("id_token_hint", user2IdToken) + .session(user1Session)) + .andExpect(status().isBadRequest()) + .andExpect(status().reason("[invalid_token] OpenID Connect 1.0 Logout Request Parameter: sub")) + .andReturn(); + + assertThat(user1Session.isInvalid()).isFalse(); + } + @Test public void requestWhenCustomTokenGeneratorThenUsed() throws Exception { this.spring.register(AuthorizationServerConfigurationWithTokenGenerator.class).autowire(); @@ -403,6 +485,11 @@ public class OidcTests { return NoOpPasswordEncoder.getInstance(); } + @Bean + SessionRegistry sessionRegistry() { + return sessionRegistry; + } + static class RowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper { RowMapper(RegisteredClientRepository registeredClientRepository) { diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProviderTests.java index 1bb0db18..4928f4c1 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationProviderTests.java @@ -126,7 +126,7 @@ public class OidcLogoutAuthenticationProviderTests { }); verify(this.authorizationService).findByToken( - eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE)); + eq(authentication.getIdTokenHint()), eq(ID_TOKEN_TOKEN_TYPE)); } @Test @@ -160,7 +160,7 @@ public class OidcLogoutAuthenticationProviderTests { assertThat(error.getDescription()).contains(IdTokenClaimNames.AUD); }); verify(this.authorizationService).findByToken( - eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE)); + eq(authentication.getIdTokenHint()), eq(ID_TOKEN_TOKEN_TYPE)); verify(this.registeredClientRepository).findById( eq(authorization.getRegisteredClientId())); } @@ -197,7 +197,7 @@ public class OidcLogoutAuthenticationProviderTests { assertThat(error.getDescription()).contains(IdTokenClaimNames.AUD); }); verify(this.authorizationService).findByToken( - eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE)); + eq(authentication.getIdTokenHint()), eq(ID_TOKEN_TOKEN_TYPE)); verify(this.registeredClientRepository).findById( eq(authorization.getRegisteredClientId())); } @@ -234,7 +234,7 @@ public class OidcLogoutAuthenticationProviderTests { assertThat(error.getDescription()).contains(OAuth2ParameterNames.CLIENT_ID); }); verify(this.authorizationService).findByToken( - eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE)); + eq(authentication.getIdTokenHint()), eq(ID_TOKEN_TOKEN_TYPE)); verify(this.registeredClientRepository).findById( eq(authorization.getRegisteredClientId())); } @@ -272,7 +272,46 @@ public class OidcLogoutAuthenticationProviderTests { assertThat(error.getDescription()).contains("post_logout_redirect_uri"); }); verify(this.authorizationService).findByToken( - eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE)); + eq(authentication.getIdTokenHint()), eq(ID_TOKEN_TOKEN_TYPE)); + verify(this.registeredClientRepository).findById( + eq(authorization.getRegisteredClientId())); + } + + @Test + public void authenticateWhenInvalidSubThenThrowOAuth2AuthenticationException() { + TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials"); + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + OidcIdToken idToken = OidcIdToken.withTokenValue("id-token") + .issuer("https://provider.com") + .subject("other-sub") + .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); + + principal.setAuthenticated(true); + + 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("sub"); + }); + verify(this.authorizationService).findByToken( + eq(authentication.getIdTokenHint()), eq(ID_TOKEN_TOKEN_TYPE)); verify(this.registeredClientRepository).findById( eq(authorization.getRegisteredClientId())); } @@ -317,7 +356,7 @@ public class OidcLogoutAuthenticationProviderTests { assertThat(error.getDescription()).contains("sid"); }); verify(this.authorizationService).findByToken( - eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE)); + eq(authentication.getIdTokenHint()), eq(ID_TOKEN_TOKEN_TYPE)); verify(this.registeredClientRepository).findById( eq(authorization.getRegisteredClientId())); } @@ -363,7 +402,7 @@ public class OidcLogoutAuthenticationProviderTests { assertThat(error.getDescription()).contains("sid"); }); verify(this.authorizationService).findByToken( - eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE)); + eq(authentication.getIdTokenHint()), eq(ID_TOKEN_TOKEN_TYPE)); verify(this.registeredClientRepository).findById( eq(authorization.getRegisteredClientId())); } @@ -408,15 +447,14 @@ public class OidcLogoutAuthenticationProviderTests { (OidcLogoutAuthenticationToken) this.authenticationProvider.authenticate(authentication); verify(this.authorizationService).findByToken( - eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE)); + eq(authentication.getIdTokenHint()), 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.getIdToken()).isEqualTo(idToken); 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); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationTokenTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationTokenTests.java index f6af09d2..3a336733 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationTokenTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcLogoutAuthenticationTokenTests.java @@ -15,13 +15,12 @@ */ 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 org.springframework.security.oauth2.core.oidc.OidcIdToken; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -32,59 +31,60 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException * @author Joe Grandja */ public class OidcLogoutAuthenticationTokenTests { - private final String idToken = "id-token"; + private final String idTokenHint = "id-token"; + private final OidcIdToken idToken = OidcIdToken.withTokenValue(this.idTokenHint) + .issuer("https://provider.com") + .subject("principal") + .issuedAt(Instant.now().minusSeconds(60)) + .expiresAt(Instant.now().plusSeconds(60)) + .build(); 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() { + public void constructorWhenIdTokenHintEmptyThenThrowIllegalArgumentException() { assertThatIllegalArgumentException() .isThrownBy(() -> new OidcLogoutAuthenticationToken( "", this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state)) - .withMessage("idToken cannot be empty"); + .withMessage("idTokenHint cannot be empty"); assertThatIllegalArgumentException() .isThrownBy(() -> new OidcLogoutAuthenticationToken( - "", this.principal, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state)) - .withMessage("idToken cannot be empty"); + (String) null, this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state)) + .withMessage("idTokenHint cannot be empty"); + } + + @Test + public void constructorWhenIdTokenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new OidcLogoutAuthenticationToken( + (OidcIdToken) null, this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state)) + .withMessage("idToken cannot be null"); } @Test public void constructorWhenPrincipalNullThenThrowIllegalArgumentException() { assertThatIllegalArgumentException() .isThrownBy(() -> new OidcLogoutAuthenticationToken( - this.idToken, null, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state)) + this.idTokenHint, 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)) + this.idToken, null, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state)) .withMessage("principal cannot be null"); } @Test - public void constructorWhenSessionIdProvidedThenCreated() { + public void constructorWhenIdTokenHintProvidedThenCreated() { OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken( - this.idToken, this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state); + this.idTokenHint, 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.getIdTokenHint()).isEqualTo(this.idTokenHint); + assertThat(authentication.getIdToken()).isNull(); 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); @@ -92,14 +92,14 @@ public class OidcLogoutAuthenticationTokenTests { } @Test - public void constructorWhenSessionInformationProvidedThenCreated() { + public void constructorWhenIdTokenProvidedThenCreated() { OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken( - this.idToken, this.principal, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state); + 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.getIdTokenHint()).isEqualTo(this.idToken.getTokenValue()); assertThat(authentication.getIdToken()).isEqualTo(this.idToken); - assertThat(authentication.getSessionId()).isEqualTo(this.sessionInformation.getSessionId()); - assertThat(authentication.getSessionInformation()).isEqualTo(this.sessionInformation); + assertThat(authentication.getSessionId()).isEqualTo(this.sessionId); assertThat(authentication.getClientId()).isEqualTo(this.clientId); assertThat(authentication.getPostLogoutRedirectUri()).isEqualTo(this.postLogoutRedirectUri); assertThat(authentication.getState()).isEqualTo(this.state); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcLogoutEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcLogoutEndpointFilterTests.java index e973b5ef..a678823e 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcLogoutEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcLogoutEndpointFilterTests.java @@ -15,8 +15,6 @@ */ 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; @@ -38,7 +36,6 @@ 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; @@ -217,7 +214,7 @@ public class OidcLogoutEndpointFilterTests { @Test public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception { OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken( - "id-token", this.principal, (SessionInformation) null, null, null, null); + "id-token", this.principal, null, null, null, null); AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class); when(authenticationConverter.convert(any())).thenReturn(authentication); @@ -240,7 +237,7 @@ public class OidcLogoutEndpointFilterTests { @Test public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception { OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken( - "id-token", this.principal, (SessionInformation) null, null, null, null); + "id-token", this.principal, null, null, null, null); AuthenticationSuccessHandler authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class); this.filter.setAuthenticationSuccessHandler(authenticationSuccessHandler); @@ -292,11 +289,8 @@ public class OidcLogoutEndpointFilterTests { 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); + "id-token", this.principal, session.getId(), null, null, null); when(this.authenticationManager.authenticate(any())) .thenReturn(authentication); @@ -321,14 +315,12 @@ public class OidcLogoutEndpointFilterTests { 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, + "id-token", this.principal, session.getId(), registeredClient.getClientId(), postLogoutRedirectUri, state); + authentication.setAuthenticated(true); when(this.authenticationManager.authenticate(any())) .thenReturn(authentication);