From baad23caab88cb7912cb718ea4a422804801a96e Mon Sep 17 00:00:00 2001 From: Joe Grandja <10884212+jgrandja@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:32:47 -0400 Subject: [PATCH] Enable null-safety in spring-security-oauth2-client Closes gh-17819 --- ...egistrationsBeanDefinitionParserTests.java | 2 +- .../login/AuthorizationEndpointDslTests.kt | 10 +- ...ntHttpInterfaceIntegrationConfiguration.kt | 2 +- .../spring-security-oauth2-client.gradle | 1 + ...ionCodeOAuth2AuthorizedClientProvider.java | 6 +- ...tServiceOAuth2AuthorizedClientManager.java | 6 +- ...entialsOAuth2AuthorizedClientProvider.java | 9 +- ...eactiveOAuth2AuthorizedClientProvider.java | 3 +- ...egatingOAuth2AuthorizedClientProvider.java | 6 +- ...InMemoryOAuth2AuthorizedClientService.java | 4 +- ...ReactiveOAuth2AuthorizedClientService.java | 9 +- .../JdbcOAuth2AuthorizedClientService.java | 26 +++- ...tBearerOAuth2AuthorizedClientProvider.java | 15 +- ...eactiveOAuth2AuthorizedClientProvider.java | 12 +- .../client/OAuth2AuthorizationContext.java | 25 ++-- .../oauth2/client/OAuth2AuthorizeRequest.java | 28 ++-- .../oauth2/client/OAuth2AuthorizedClient.java | 5 +- .../client/OAuth2AuthorizedClientManager.java | 6 +- .../OAuth2AuthorizedClientProvider.java | 6 +- ...OAuth2AuthorizedClientProviderBuilder.java | 16 +- .../client/OAuth2AuthorizedClientService.java | 5 +- ...ReactiveOAuth2AuthorizedClientService.java | 68 +++++---- ...OAuth2AuthorizedClientProviderBuilder.java | 16 +- ...tiveOAuth2AuthorizationSuccessHandler.java | 27 ++-- ...shTokenOAuth2AuthorizedClientProvider.java | 11 +- ...eactiveOAuth2AuthorizedClientProvider.java | 3 +- ...xchangeOAuth2AuthorizedClientProvider.java | 20 +-- ...eactiveOAuth2AuthorizedClientProvider.java | 8 +- .../client/annotation/package-info.java | 23 +++ .../oauth2/client/aot/hint/package-info.java | 23 +++ ...thorizationCodeAuthenticationProvider.java | 8 +- ...2AuthorizationCodeAuthenticationToken.java | 13 +- ...tionCodeReactiveAuthenticationManager.java | 7 +- .../OAuth2LoginAuthenticationProvider.java | 6 +- .../OAuth2LoginAuthenticationToken.java | 13 +- ...th2LoginReactiveAuthenticationManager.java | 1 + .../client/authentication/package-info.java | 3 + ...OAuth2TokenRequestParametersConverter.java | 6 +- ...ientAuthenticationParametersConverter.java | 7 +- .../endpoint/TokenExchangeGrantRequest.java | 10 +- .../oauth2/client/endpoint/package-info.java | 3 + .../oauth2/client/event/package-info.java | 23 +++ .../http/OAuth2ErrorResponseErrorHandler.java | 5 +- .../oauth2/client/http/package-info.java | 23 +++ .../ClientRegistrationDeserializer.java | 53 +++++-- .../oauth2/client/jackson/JsonNodeUtils.java | 7 +- ...Auth2AuthorizationRequestDeserializer.java | 23 ++- .../oauth2/client/jackson/StdConverters.java | 16 +- .../oauth2/client/jackson/package-info.java | 3 + .../ClientRegistrationDeserializer.java | 51 +++++-- .../oauth2/client/jackson2/JsonNodeUtils.java | 7 +- ...Auth2AuthorizationRequestDeserializer.java | 25 +++- .../oauth2/client/jackson2/StdConverters.java | 16 +- .../oauth2/client/jackson2/package-info.java | 3 + ...thorizationCodeAuthenticationProvider.java | 17 ++- ...tionCodeReactiveAuthenticationManager.java | 9 +- ...uthorizedClientRefreshedEventListener.java | 40 +++-- .../authentication/OidcIdTokenValidator.java | 22 +-- .../authentication/event/package-info.java | 23 +++ .../logout/LogoutTokenClaimAccessor.java | 25 +++- .../logout/OidcLogoutToken.java | 11 +- .../authentication/logout/package-info.java | 23 +++ .../oidc/authentication/package-info.java | 3 + .../oidc/server/session/package-info.java | 23 +++ .../session/InMemoryOidcSessionRegistry.java | 19 ++- .../client/oidc/session/package-info.java | 23 +++ .../client/oidc/userinfo/OidcUserService.java | 1 + .../client/oidc/userinfo/package-info.java | 3 + ...dcClientInitiatedLogoutSuccessHandler.java | 34 +++-- .../client/oidc/web/logout/package-info.java | 23 +++ ...ntInitiatedServerLogoutSuccessHandler.java | 14 +- .../oidc/web/server/logout/package-info.java | 23 +++ .../security/oauth2/client/package-info.java | 3 + .../registration/ClientRegistration.java | 137 ++++++++++-------- .../ClientRegistrationRepository.java | 4 +- .../registration/ClientRegistrations.java | 20 ++- .../InMemoryClientRegistrationRepository.java | 4 +- .../SupplierClientRegistrationRepository.java | 4 +- .../client/registration/package-info.java | 3 + .../userinfo/DefaultOAuth2UserService.java | 4 +- .../DefaultReactiveOAuth2UserService.java | 11 +- .../userinfo/DelegatingOAuth2UserService.java | 4 +- .../OAuth2UserRequestEntityConverter.java | 8 +- .../client/userinfo/OAuth2UserService.java | 4 +- .../oauth2/client/userinfo/package-info.java | 3 + ...cipalOAuth2AuthorizedClientRepository.java | 3 +- .../web/AuthorizationRequestRepository.java | 5 +- .../oauth2/client/web/ClientAttributes.java | 4 +- ...ultOAuth2AuthorizationRequestResolver.java | 16 +- .../DefaultOAuth2AuthorizedClientManager.java | 43 ++++-- ...ReactiveOAuth2AuthorizedClientManager.java | 21 ++- ...nOAuth2AuthorizationRequestRepository.java | 9 +- ...ssionOAuth2AuthorizedClientRepository.java | 3 +- .../OAuth2AuthorizationCodeGrantFilter.java | 19 ++- ...th2AuthorizationRequestRedirectFilter.java | 18 ++- .../OAuth2AuthorizationRequestResolver.java | 5 +- .../web/OAuth2AuthorizationResponseUtils.java | 30 ++-- .../web/OAuth2AuthorizedClientRepository.java | 5 +- .../web/OAuth2LoginAuthenticationFilter.java | 3 + .../OAuth2ClientHttpRequestInterceptor.java | 20 +-- ...AttributeClientRegistrationIdResolver.java | 4 +- .../RequestAttributePrincipalResolver.java | 6 +- ...ecurityContextHolderPrincipalResolver.java | 4 +- .../client/web/client/package-info.java | 23 +++ .../web/client/support/package-info.java | 23 +++ ...Auth2AuthorizedClientArgumentResolver.java | 13 +- .../web/method/annotation/package-info.java | 23 +++ .../oauth2/client/web/package-info.java | 3 + ...uthorizedClientExchangeFilterFunction.java | 30 ++-- ...uthorizedClientExchangeFilterFunction.java | 70 +++++---- .../function/client/package-info.java | 23 +++ .../function/client/support/package-info.java | 23 +++ ...Auth2AuthorizedClientArgumentResolver.java | 9 +- .../method/annotation/package-info.java | 23 +++ ...verOAuth2AuthorizationRequestResolver.java | 13 +- ...OAuth2AuthorizationCodeGrantWebFilter.java | 13 +- .../OAuth2AuthorizationResponseUtils.java | 30 ++-- ...2ServerAuthorizationRequestRepository.java | 12 +- .../OAuth2LoginAuthenticationWebFilter.java | 2 + .../server/authentication/package-info.java | 23 +++ .../client/web/server/package-info.java | 23 +++ .../registration/ClientRegistrationTests.java | 6 - .../ClientRegistrationsTests.java | 2 +- ...RegistrationIdProcessorWebClientTests.java | 12 +- .../server/SecurityMockServerConfigurers.java | 10 +- .../SecurityMockMvcRequestPostProcessors.java | 9 +- 126 files changed, 1381 insertions(+), 533 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/aot/hint/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/event/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/event/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/support/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/support/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/authentication/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/package-info.java diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java index 05eb9396d4..c915586930 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java @@ -157,7 +157,7 @@ public class ClientRegistrationsBeanDefinitionParserTests { .isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); assertThat(googleRegistration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); assertThat(googleRegistration.getRedirectUri()).isEqualTo("{baseUrl}/{action}/oauth2/code/{registrationId}"); - assertThat(googleRegistration.getScopes()).isNull(); + assertThat(googleRegistration.getScopes()).isEmpty(); assertThat(googleRegistration.getClientName()).isEqualTo(serverUrl); ProviderDetails googleProviderDetails = googleRegistration.getProviderDetails(); assertThat(googleProviderDetails).isNotNull(); diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/login/AuthorizationEndpointDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/login/AuthorizationEndpointDslTests.kt index dbca4bf7e2..fac0c4eca4 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/login/AuthorizationEndpointDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/login/AuthorizationEndpointDslTests.kt @@ -73,13 +73,11 @@ class AuthorizationEndpointDslTests { companion object { val RESOLVER: OAuth2AuthorizationRequestResolver = object : OAuth2AuthorizationRequestResolver { - override fun resolve( - request: HttpServletRequest? - ) = OAuth2AuthorizationRequest.authorizationCode().build() + override fun resolve(request: HttpServletRequest) = + OAuth2AuthorizationRequest.authorizationCode().build() - override fun resolve( - request: HttpServletRequest?, clientRegistrationId: String? - ) = OAuth2AuthorizationRequest.authorizationCode().build() + override fun resolve(request: HttpServletRequest, clientRegistrationId: String) = + OAuth2AuthorizationRequest.authorizationCode().build() } } diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/features/integrations/rest/configurationwebclient/ServerWebClientHttpInterfaceIntegrationConfiguration.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/features/integrations/rest/configurationwebclient/ServerWebClientHttpInterfaceIntegrationConfiguration.kt index f912a28689..1675ef2aed 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/features/integrations/rest/configurationwebclient/ServerWebClientHttpInterfaceIntegrationConfiguration.kt +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/features/integrations/rest/configurationwebclient/ServerWebClientHttpInterfaceIntegrationConfiguration.kt @@ -43,7 +43,7 @@ class ServerWebClientHttpInterfaceIntegrationConfiguration { fun securityConfigurer( manager: ReactiveOAuth2AuthorizedClientManager? ): OAuth2WebClientHttpServiceGroupConfigurer { - return OAuth2WebClientHttpServiceGroupConfigurer.from(manager) + return OAuth2WebClientHttpServiceGroupConfigurer.from(requireNotNull(manager)) } // end::config[] diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index 11f40428de..0db593974d 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -1,5 +1,6 @@ plugins { id 'javadoc-warnings-error' + id 'security-nullability' } apply plugin: 'io.spring.convention.spring-module' diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizationCodeOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizationCodeOAuth2AuthorizedClientProvider.java index 68f1681c62..c004ddbf07 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizationCodeOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizationCodeOAuth2AuthorizedClientProvider.java @@ -16,7 +16,8 @@ package org.springframework.security.oauth2.client; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -47,8 +48,7 @@ public final class AuthorizationCodeOAuth2AuthorizedClientProvider implements OA * the authorization request */ @Override - @Nullable - public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { Assert.notNull(context, "context cannot be null"); if (AuthorizationGrantType.AUTHORIZATION_CODE.equals( context.getClientRegistration().getAuthorizationGrantType()) && context.getAuthorizedClient() == null) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceOAuth2AuthorizedClientManager.java index 9087740895..3bb7813204 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceOAuth2AuthorizedClientManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceOAuth2AuthorizedClientManager.java @@ -21,7 +21,8 @@ import java.util.HashMap; import java.util.Map; import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -113,9 +114,8 @@ public final class AuthorizedClientServiceOAuth2AuthorizedClientManager implemen .removeAuthorizedClient(clientRegistrationId, principal.getName())); } - @Nullable @Override - public OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest) { + public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest) { Assert.notNull(authorizeRequest, "authorizeRequest cannot be null"); String clientRegistrationId = authorizeRequest.getClientRegistrationId(); OAuth2AuthorizedClient authorizedClient = authorizeRequest.getAuthorizedClient(); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientCredentialsOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientCredentialsOAuth2AuthorizedClientProvider.java index 90143314c7..d21f2ed8c5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientCredentialsOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientCredentialsOAuth2AuthorizedClientProvider.java @@ -20,7 +20,8 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; import org.springframework.security.oauth2.client.endpoint.RestClientClientCredentialsTokenResponseClient; @@ -61,8 +62,7 @@ public final class ClientCredentialsOAuth2AuthorizedClientProvider implements OA * re-authorization) is not supported */ @Override - @Nullable - public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { Assert.notNull(context, "context cannot be null"); ClientRegistration clientRegistration = context.getClientRegistration(); if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType())) { @@ -98,7 +98,8 @@ public final class ClientCredentialsOAuth2AuthorizedClientProvider implements OA } private boolean hasTokenExpired(OAuth2Token token) { - return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + Instant expiresAt = token.getExpiresAt(); + return expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew)); } /** diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientCredentialsReactiveOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientCredentialsReactiveOAuth2AuthorizedClientProvider.java index 5448199a0e..aa23124942 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientCredentialsReactiveOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientCredentialsReactiveOAuth2AuthorizedClientProvider.java @@ -89,7 +89,8 @@ public final class ClientCredentialsReactiveOAuth2AuthorizedClientProvider } private boolean hasTokenExpired(OAuth2Token token) { - return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + Instant expiresAt = token.getExpiresAt(); + return expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew)); } /** diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/DelegatingOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/DelegatingOAuth2AuthorizedClientProvider.java index cddf44c91f..d51628b23a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/DelegatingOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/DelegatingOAuth2AuthorizedClientProvider.java @@ -21,7 +21,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -64,8 +65,7 @@ public final class DelegatingOAuth2AuthorizedClientProvider implements OAuth2Aut } @Override - @Nullable - public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { Assert.notNull(context, "context cannot be null"); for (OAuth2AuthorizedClientProvider authorizedClientProvider : this.authorizedClientProviders) { OAuth2AuthorizedClient oauth2AuthorizedClient = authorizedClientProvider.authorize(context); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/InMemoryOAuth2AuthorizedClientService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/InMemoryOAuth2AuthorizedClientService.java index 2baba5aa37..8cd6a4aa76 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/InMemoryOAuth2AuthorizedClientService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/InMemoryOAuth2AuthorizedClientService.java @@ -19,6 +19,8 @@ package org.springframework.security.oauth2.client; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -72,7 +74,7 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author @Override @SuppressWarnings("unchecked") - public T loadAuthorizedClient(String clientRegistrationId, + public @Nullable T loadAuthorizedClient(String clientRegistrationId, String principalName) { Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); Assert.hasText(principalName, "principalName cannot be empty"); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientService.java index 84b0bdd32e..fa61eacebc 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/InMemoryReactiveOAuth2AuthorizedClientService.java @@ -62,14 +62,15 @@ public final class InMemoryReactiveOAuth2AuthorizedClientService implements Reac Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); Assert.hasText(principalName, "principalName cannot be empty"); return (Mono) this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId) - .mapNotNull((clientRegistration) -> { + .flatMap((clientRegistration) -> { OAuth2AuthorizedClientId id = new OAuth2AuthorizedClientId(clientRegistrationId, principalName); OAuth2AuthorizedClient cachedAuthorizedClient = this.authorizedClients.get(id); if (cachedAuthorizedClient == null) { - return null; + return Mono.empty(); } - return new OAuth2AuthorizedClient(clientRegistration, cachedAuthorizedClient.getPrincipalName(), - cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken()); + return Mono + .just(new OAuth2AuthorizedClient(clientRegistration, cachedAuthorizedClient.getPrincipalName(), + cachedAuthorizedClient.getAccessToken(), cachedAuthorizedClient.getRefreshToken())); }); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java index 5397180727..1a305ed867 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java @@ -29,6 +29,8 @@ import java.util.List; import java.util.Set; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataRetrievalFailureException; import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; import org.springframework.jdbc.core.JdbcOperations; @@ -148,7 +150,7 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient @Override @SuppressWarnings("unchecked") - public T loadAuthorizedClient(String clientRegistrationId, + public @Nullable T loadAuthorizedClient(String clientRegistrationId, String principalName) { Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); Assert.hasText(principalName, "principalName cannot be empty"); @@ -265,16 +267,21 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(rs.getString("access_token_type"))) { tokenType = OAuth2AccessToken.TokenType.BEARER; } + OAuth2AccessToken.TokenType tokenTypeToUse = (tokenType != null) ? tokenType + : OAuth2AccessToken.TokenType.BEARER; String tokenValue = new String(this.lobHandler.getBlobAsBytes(rs, "access_token_value"), StandardCharsets.UTF_8); - Instant issuedAt = rs.getTimestamp("access_token_issued_at").toInstant(); - Instant expiresAt = rs.getTimestamp("access_token_expires_at").toInstant(); + Timestamp issuedAtTs = rs.getTimestamp("access_token_issued_at"); + Timestamp expiresAtTs = rs.getTimestamp("access_token_expires_at"); + Instant issuedAt = (issuedAtTs != null) ? issuedAtTs.toInstant() : null; + Instant expiresAt = (expiresAtTs != null) ? expiresAtTs.toInstant() : null; Set scopes = Collections.emptySet(); String accessTokenScopes = rs.getString("access_token_scopes"); if (accessTokenScopes != null) { scopes = StringUtils.commaDelimitedListToSet(accessTokenScopes); } - OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, tokenValue, issuedAt, expiresAt, scopes); + OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenTypeToUse, tokenValue, issuedAt, expiresAt, + scopes); OAuth2RefreshToken refreshToken = null; byte[] refreshTokenValue = this.lobHandler.getBlobAsBytes(rs, "refresh_token_value"); if (refreshTokenValue != null) { @@ -312,8 +319,12 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient parameters.add(new SqlParameterValue(Types.VARCHAR, accessToken.getTokenType().getValue())); parameters .add(new SqlParameterValue(Types.BLOB, accessToken.getTokenValue().getBytes(StandardCharsets.UTF_8))); - parameters.add(new SqlParameterValue(Types.TIMESTAMP, Timestamp.from(accessToken.getIssuedAt()))); - parameters.add(new SqlParameterValue(Types.TIMESTAMP, Timestamp.from(accessToken.getExpiresAt()))); + Instant accessTokenIssuedAt = accessToken.getIssuedAt(); + Instant accessTokenExpiresAt = accessToken.getExpiresAt(); + parameters.add(new SqlParameterValue(Types.TIMESTAMP, + (accessTokenIssuedAt != null) ? Timestamp.from(accessTokenIssuedAt) : null)); + parameters.add(new SqlParameterValue(Types.TIMESTAMP, + (accessTokenExpiresAt != null) ? Timestamp.from(accessTokenExpiresAt) : null)); String accessTokenScopes = null; if (!CollectionUtils.isEmpty(accessToken.getScopes())) { accessTokenScopes = StringUtils.collectionToDelimitedString(accessToken.getScopes(), ","); @@ -385,7 +396,8 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient } @Override - protected void doSetValue(PreparedStatement ps, int parameterPosition, Object argValue) throws SQLException { + protected void doSetValue(PreparedStatement ps, int parameterPosition, @Nullable Object argValue) + throws SQLException { if (argValue instanceof SqlParameterValue paramValue) { if (paramValue.getSqlType() == Types.BLOB) { if (paramValue.getValue() != null) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java index 2533b95506..89257b0a18 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java @@ -21,7 +21,8 @@ import java.time.Duration; import java.time.Instant; import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.RestClientJwtBearerTokenResponseClient; @@ -46,7 +47,7 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth private OAuth2AccessTokenResponseClient accessTokenResponseClient = new RestClientJwtBearerTokenResponseClient(); - private Function jwtAssertionResolver = this::resolveJwtAssertion; + private Function jwtAssertionResolver = this::resolveJwtAssertion; private Duration clockSkew = Duration.ofSeconds(60); @@ -65,8 +66,7 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth * supported */ @Override - @Nullable - public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { Assert.notNull(context, "context cannot be null"); ClientRegistration clientRegistration = context.getClientRegistration(); if (!AuthorizationGrantType.JWT_BEARER.equals(clientRegistration.getAuthorizationGrantType())) { @@ -100,7 +100,7 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth tokenResponse.getAccessToken()); } - private Jwt resolveJwtAssertion(OAuth2AuthorizationContext context) { + private @Nullable Jwt resolveJwtAssertion(OAuth2AuthorizationContext context) { if (!(context.getPrincipal().getPrincipal() instanceof Jwt)) { return null; } @@ -118,7 +118,8 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth } private boolean hasTokenExpired(OAuth2Token token) { - return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + Instant expiresAt = token.getExpiresAt(); + return expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew)); } /** @@ -139,7 +140,7 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth * assertion * @since 5.7 */ - public void setJwtAssertionResolver(Function jwtAssertionResolver) { + public void setJwtAssertionResolver(Function jwtAssertionResolver) { Assert.notNull(jwtAssertionResolver, "jwtAssertionResolver cannot be null"); this.jwtAssertionResolver = jwtAssertionResolver; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java index e090e53929..851880c9d2 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java @@ -106,14 +106,18 @@ public final class JwtBearerReactiveOAuth2AuthorizedClientProvider implements Re private Mono resolveJwtAssertion(OAuth2AuthorizationContext context) { // @formatter:off return Mono.just(context) - .map((ctx) -> ctx.getPrincipal().getPrincipal()) - .filter((principal) -> principal instanceof Jwt) - .cast(Jwt.class); + .flatMap((ctx) -> { + Object principal = ctx.getPrincipal().getPrincipal(); + return (principal instanceof Jwt) + ? Mono.just((Jwt) principal) + : Mono.empty(); + }); // @formatter:on } private boolean hasTokenExpired(OAuth2Token token) { - return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + Instant expiresAt = token.getExpiresAt(); + return expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew)); } /** diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java index 7b0b952e7f..bda8d8a1ab 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizationContext.java @@ -22,7 +22,8 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.util.Assert; @@ -48,13 +49,13 @@ public final class OAuth2AuthorizationContext { public static final String REQUEST_SCOPE_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName() .concat(".REQUEST_SCOPE"); - private ClientRegistration clientRegistration; + private @Nullable ClientRegistration clientRegistration; - private OAuth2AuthorizedClient authorizedClient; + private @Nullable OAuth2AuthorizedClient authorizedClient; - private Authentication principal; + private @Nullable Authentication principal; - private Map attributes; + private @Nullable Map attributes; private OAuth2AuthorizationContext() { } @@ -64,6 +65,7 @@ public final class OAuth2AuthorizationContext { * @return the {@link ClientRegistration} */ public ClientRegistration getClientRegistration() { + Assert.notNull(this.clientRegistration, "clientRegistration cannot be null"); return this.clientRegistration; } @@ -74,8 +76,7 @@ public final class OAuth2AuthorizationContext { * @return the {@link OAuth2AuthorizedClient} or {@code null} if the client * registration was supplied */ - @Nullable - public OAuth2AuthorizedClient getAuthorizedClient() { + public @Nullable OAuth2AuthorizedClient getAuthorizedClient() { return this.authorizedClient; } @@ -84,6 +85,7 @@ public final class OAuth2AuthorizationContext { * @return the {@code Principal} (to be) associated to the authorized client */ public Authentication getPrincipal() { + Assert.notNull(this.principal, "principal cannot be null"); return this.principal; } @@ -92,6 +94,7 @@ public final class OAuth2AuthorizationContext { * @return a {@code Map} of the attributes associated to the context */ public Map getAttributes() { + Assert.notNull(this.attributes, "attributes cannot be null"); return this.attributes; } @@ -131,13 +134,13 @@ public final class OAuth2AuthorizationContext { */ public static final class Builder { - private ClientRegistration clientRegistration; + private @Nullable ClientRegistration clientRegistration; - private OAuth2AuthorizedClient authorizedClient; + private @Nullable OAuth2AuthorizedClient authorizedClient; - private Authentication principal; + private @Nullable Authentication principal; - private Map attributes; + private @Nullable Map attributes; private Builder(ClientRegistration clientRegistration) { Assert.notNull(clientRegistration, "clientRegistration cannot be null"); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizeRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizeRequest.java index 82184888b3..629ba83e1b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizeRequest.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizeRequest.java @@ -23,7 +23,8 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -43,13 +44,13 @@ import org.springframework.util.CollectionUtils; */ public final class OAuth2AuthorizeRequest { - private String clientRegistrationId; + private @Nullable String clientRegistrationId; - private OAuth2AuthorizedClient authorizedClient; + private @Nullable OAuth2AuthorizedClient authorizedClient; - private Authentication principal; + private @Nullable Authentication principal; - private Map attributes; + private @Nullable Map attributes; private OAuth2AuthorizeRequest() { } @@ -59,6 +60,7 @@ public final class OAuth2AuthorizeRequest { * @return the identifier for the client registration */ public String getClientRegistrationId() { + Assert.notNull(this.clientRegistrationId, "clientRegistrationId cannot be null"); return this.clientRegistrationId; } @@ -67,8 +69,7 @@ public final class OAuth2AuthorizeRequest { * was not provided. * @return the {@link OAuth2AuthorizedClient} or {@code null} if it was not provided */ - @Nullable - public OAuth2AuthorizedClient getAuthorizedClient() { + public @Nullable OAuth2AuthorizedClient getAuthorizedClient() { return this.authorizedClient; } @@ -77,6 +78,7 @@ public final class OAuth2AuthorizeRequest { * @return the {@code Principal} (to be) associated to the authorized client */ public Authentication getPrincipal() { + Assert.notNull(this.principal, "principal cannot be null"); return this.principal; } @@ -85,6 +87,7 @@ public final class OAuth2AuthorizeRequest { * @return a {@code Map} of the attributes associated to the request */ public Map getAttributes() { + Assert.notNull(this.attributes, "attributes cannot be null"); return this.attributes; } @@ -95,9 +98,8 @@ public final class OAuth2AuthorizeRequest { * @param the type of the attribute * @return the value of the attribute associated to the request */ - @Nullable @SuppressWarnings("unchecked") - public T getAttribute(String name) { + public @Nullable T getAttribute(String name) { return (T) this.getAttributes().get(name); } @@ -127,13 +129,13 @@ public final class OAuth2AuthorizeRequest { */ public static final class Builder { - private String clientRegistrationId; + private @Nullable String clientRegistrationId; - private OAuth2AuthorizedClient authorizedClient; + private @Nullable OAuth2AuthorizedClient authorizedClient; - private Authentication principal; + private @Nullable Authentication principal; - private Map attributes; + private @Nullable Map attributes; private Builder(String clientRegistrationId) { Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java index 47091bf168..4227253711 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClient.java @@ -18,7 +18,8 @@ package org.springframework.security.oauth2.client; import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; @@ -50,7 +51,7 @@ public class OAuth2AuthorizedClient implements Serializable { private final OAuth2AccessToken accessToken; - private final OAuth2RefreshToken refreshToken; + private final @Nullable OAuth2RefreshToken refreshToken; /** * Constructs an {@code OAuth2AuthorizedClient} using the provided parameters. diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.java index 0cf99029df..6d20223855 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.java @@ -16,7 +16,8 @@ package org.springframework.security.oauth2.client; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; @@ -62,7 +63,6 @@ public interface OAuth2AuthorizedClientManager { * @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not * supported for the specified client */ - @Nullable - OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest); + @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProvider.java index 42761789f4..7306fa8ad5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProvider.java @@ -16,7 +16,8 @@ package org.springframework.security.oauth2.client; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -46,7 +47,6 @@ public interface OAuth2AuthorizedClientProvider { * @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not * supported for the specified client */ - @Nullable - OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context); + @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java index 157a8a531b..c1156e3241 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java @@ -25,6 +25,8 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; @@ -157,11 +159,11 @@ public final class OAuth2AuthorizedClientProviderBuilder { */ public final class ClientCredentialsGrantBuilder implements Builder { - private OAuth2AccessTokenResponseClient accessTokenResponseClient; + private @Nullable OAuth2AccessTokenResponseClient accessTokenResponseClient; - private Duration clockSkew; + private @Nullable Duration clockSkew; - private Clock clock; + private @Nullable Clock clock; private ClientCredentialsGrantBuilder() { } @@ -249,13 +251,13 @@ public final class OAuth2AuthorizedClientProviderBuilder { */ public final class RefreshTokenGrantBuilder implements Builder { - private OAuth2AccessTokenResponseClient accessTokenResponseClient; + private @Nullable OAuth2AccessTokenResponseClient accessTokenResponseClient; - private ApplicationEventPublisher eventPublisher; + private @Nullable ApplicationEventPublisher eventPublisher; - private Duration clockSkew; + private @Nullable Duration clockSkew; - private Clock clock; + private @Nullable Clock clock; private RefreshTokenGrantBuilder() { } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientService.java index ad8bdf2c84..e93cdc394d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientService.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.client; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -46,7 +48,8 @@ public interface OAuth2AuthorizedClientService { * @param a type of OAuth2AuthorizedClient * @return the {@link OAuth2AuthorizedClient} or {@code null} if not available */ - T loadAuthorizedClient(String clientRegistrationId, String principalName); + @Nullable T loadAuthorizedClient(String clientRegistrationId, + String principalName); /** * Saves the {@link OAuth2AuthorizedClient} associating it to the provided End-User diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/R2dbcReactiveOAuth2AuthorizedClientService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/R2dbcReactiveOAuth2AuthorizedClientService.java index eba08edab7..66401d14e2 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/R2dbcReactiveOAuth2AuthorizedClientService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/R2dbcReactiveOAuth2AuthorizedClientService.java @@ -31,6 +31,7 @@ import java.util.function.Function; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.dao.DataRetrievalFailureException; @@ -240,7 +241,7 @@ public class R2dbcReactiveOAuth2AuthorizedClientService implements ReactiveOAuth private final OAuth2AccessToken accessToken; - private final OAuth2RefreshToken refreshToken; + private final @Nullable OAuth2RefreshToken refreshToken; /** * Constructs an {@code OAuth2AuthorizedClientHolder} using the provided @@ -266,7 +267,7 @@ public class R2dbcReactiveOAuth2AuthorizedClientService implements ReactiveOAuth * @param refreshToken the refresh token */ public OAuth2AuthorizedClientHolder(String clientRegistrationId, String principalName, - OAuth2AccessToken accessToken, OAuth2RefreshToken refreshToken) { + OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) { Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); Assert.hasText(principalName, "principalName cannot be empty"); Assert.notNull(accessToken, "accessToken cannot be null"); @@ -288,7 +289,7 @@ public class R2dbcReactiveOAuth2AuthorizedClientService implements ReactiveOAuth return this.accessToken; } - public OAuth2RefreshToken getRefreshToken() { + public @Nullable OAuth2RefreshToken getRefreshToken() { return this.refreshToken; } @@ -317,10 +318,16 @@ public class R2dbcReactiveOAuth2AuthorizedClientService implements ReactiveOAuth Parameter.fromOrEmpty(accessToken.getTokenType().getValue(), String.class)); parameters.put("accessTokenValue", Parameter.fromOrEmpty( ByteBuffer.wrap(accessToken.getTokenValue().getBytes(StandardCharsets.UTF_8)), ByteBuffer.class)); - parameters.put("accessTokenIssuedAt", Parameter - .fromOrEmpty(LocalDateTime.ofInstant(accessToken.getIssuedAt(), ZoneOffset.UTC), LocalDateTime.class)); - parameters.put("accessTokenExpiresAt", Parameter - .fromOrEmpty(LocalDateTime.ofInstant(accessToken.getExpiresAt(), ZoneOffset.UTC), LocalDateTime.class)); + Instant accessTokenIssuedAt = accessToken.getIssuedAt(); + Instant accessTokenExpiresAt = accessToken.getExpiresAt(); + parameters.put("accessTokenIssuedAt", Parameter.fromOrEmpty( + (accessTokenIssuedAt != null) ? LocalDateTime.ofInstant(accessTokenIssuedAt, ZoneOffset.UTC) : null, + LocalDateTime.class)); + parameters.put("accessTokenExpiresAt", + Parameter.fromOrEmpty( + (accessTokenExpiresAt != null) + ? LocalDateTime.ofInstant(accessTokenExpiresAt, ZoneOffset.UTC) : null, + LocalDateTime.class)); String accessTokenScopes = null; if (!CollectionUtils.isEmpty(accessToken.getScopes())) { accessTokenScopes = StringUtils.collectionToDelimitedString(accessToken.getScopes(), ","); @@ -353,17 +360,29 @@ public class R2dbcReactiveOAuth2AuthorizedClientService implements ReactiveOAuth @Override public OAuth2AuthorizedClientHolder apply(Row row, RowMetadata rowMetadata) { - - String dbClientRegistrationId = row.get("client_registration_id", String.class); - OAuth2AccessToken.TokenType tokenType = null; - if (OAuth2AccessToken.TokenType.BEARER.getValue() - .equalsIgnoreCase(row.get("access_token_type", String.class))) { - tokenType = OAuth2AccessToken.TokenType.BEARER; + String clientRegistrationId = row.get("client_registration_id", String.class); + Assert.hasText(clientRegistrationId, "client_registration_id cannot be empty"); + String principalName = row.get("principal_name", String.class); + Assert.hasText(principalName, "principal_name cannot be empty"); + OAuth2AccessToken.TokenType tokenType = OAuth2AccessToken.TokenType.BEARER; + String accessTokenType = row.get("access_token_type", String.class); + if (StringUtils.hasText(accessTokenType) + && !OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(accessTokenType)) { + tokenType = new OAuth2AccessToken.TokenType(accessTokenType); + } + ByteBuffer accessTokenValueBuffer = row.get("access_token_value", ByteBuffer.class); + Assert.notNull(accessTokenValueBuffer, "access_token_value cannot be null"); + String tokenValue = new String(accessTokenValueBuffer.array(), StandardCharsets.UTF_8); + Instant issuedAt = null; + LocalDateTime issuedAtLdt = row.get("access_token_issued_at", LocalDateTime.class); + if (issuedAtLdt != null) { + issuedAt = issuedAtLdt.toInstant(ZoneOffset.UTC); + } + Instant expiresAt = null; + LocalDateTime expiresAtLdt = row.get("access_token_expires_at", LocalDateTime.class); + if (expiresAtLdt != null) { + expiresAt = expiresAtLdt.toInstant(ZoneOffset.UTC); } - String tokenValue = new String(row.get("access_token_value", ByteBuffer.class).array(), - StandardCharsets.UTF_8); - Instant issuedAt = row.get("access_token_issued_at", LocalDateTime.class).toInstant(ZoneOffset.UTC); - Instant expiresAt = row.get("access_token_expires_at", LocalDateTime.class).toInstant(ZoneOffset.UTC); Set scopes = Collections.emptySet(); String accessTokenScopes = row.get("access_token_scopes", String.class); @@ -374,19 +393,18 @@ public class R2dbcReactiveOAuth2AuthorizedClientService implements ReactiveOAuth scopes); OAuth2RefreshToken refreshToken = null; - ByteBuffer refreshTokenValue = row.get("refresh_token_value", ByteBuffer.class); - if (refreshTokenValue != null) { - tokenValue = new String(refreshTokenValue.array(), StandardCharsets.UTF_8); + ByteBuffer refreshTokenValueBuffer = row.get("refresh_token_value", ByteBuffer.class); + if (refreshTokenValueBuffer != null) { + tokenValue = new String(refreshTokenValueBuffer.array(), StandardCharsets.UTF_8); issuedAt = null; - LocalDateTime refreshTokenIssuedAt = row.get("refresh_token_issued_at", LocalDateTime.class); - if (refreshTokenIssuedAt != null) { - issuedAt = refreshTokenIssuedAt.toInstant(ZoneOffset.UTC); + issuedAtLdt = row.get("refresh_token_issued_at", LocalDateTime.class); + if (issuedAtLdt != null) { + issuedAt = issuedAtLdt.toInstant(ZoneOffset.UTC); } refreshToken = new OAuth2RefreshToken(tokenValue, issuedAt); } - String dbPrincipalName = row.get("principal_name", String.class); - return new OAuth2AuthorizedClientHolder(dbClientRegistrationId, dbPrincipalName, accessToken, refreshToken); + return new OAuth2AuthorizedClientHolder(clientRegistrationId, principalName, accessToken, refreshToken); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java index ee25f44c8d..e425737dc7 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java @@ -25,6 +25,8 @@ import java.util.Map; import java.util.function.Consumer; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; @@ -178,11 +180,11 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder { */ public final class ClientCredentialsGrantBuilder implements Builder { - private ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + private @Nullable ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; - private Duration clockSkew; + private @Nullable Duration clockSkew; - private Clock clock; + private @Nullable Clock clock; private ClientCredentialsGrantBuilder() { } @@ -252,13 +254,13 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder { */ public final class RefreshTokenGrantBuilder implements Builder { - private ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; + private @Nullable ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; - private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler; + private @Nullable ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler; - private Duration clockSkew; + private @Nullable Duration clockSkew; - private Clock clock; + private @Nullable Clock clock; private RefreshTokenGrantBuilder() { } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java index 050203f2db..c62b6c41fb 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java @@ -16,13 +16,15 @@ package org.springframework.security.oauth2.client; +import java.net.URL; import java.time.Duration; +import java.time.Instant; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.Objects; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.security.core.Authentication; @@ -191,7 +193,7 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler this.clockSkew = clockSkew; } - private String extractIdToken(Map attributes) { + private @Nullable String extractIdToken(Map attributes) { if (attributes.get(OidcParameterNames.ID_TOKEN) instanceof String idToken) { return idToken; } @@ -224,7 +226,10 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler } private void validateIssuer(OidcUser existingOidcUser, OidcIdToken idToken) { - if (!idToken.getIssuer().toString().equals(existingOidcUser.getIdToken().getIssuer().toString())) { + URL idTokenIssuer = idToken.getIssuer(); + URL existingIdTokenIssuer = existingOidcUser.getIdToken().getIssuer(); + if (idTokenIssuer == null || existingIdTokenIssuer == null + || !idTokenIssuer.toString().equals(existingIdTokenIssuer.toString())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issuer", REFRESH_TOKEN_RESPONSE_ERROR_URI); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); @@ -232,7 +237,7 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler } private void validateSubject(OidcUser existingOidcUser, OidcIdToken idToken) { - if (!idToken.getSubject().equals(existingOidcUser.getIdToken().getSubject())) { + if (!Objects.equals(idToken.getSubject(), existingOidcUser.getIdToken().getSubject())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid subject", REFRESH_TOKEN_RESPONSE_ERROR_URI); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); @@ -240,7 +245,10 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler } private void validateIssuedAt(OidcUser existingOidcUser, OidcIdToken idToken) { - if (!idToken.getIssuedAt().isAfter(existingOidcUser.getIdToken().getIssuedAt().minus(this.clockSkew))) { + Instant idTokenIssuedAt = idToken.getIssuedAt(); + Instant existingIdTokenIssuedAt = existingOidcUser.getIdToken().getIssuedAt(); + if (idTokenIssuedAt == null || existingIdTokenIssuedAt == null + || !idTokenIssuedAt.isAfter(existingIdTokenIssuedAt.minus(this.clockSkew))) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issued at time", REFRESH_TOKEN_RESPONSE_ERROR_URI); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); @@ -257,12 +265,13 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler private boolean isValidAudience(OidcUser existingOidcUser, OidcIdToken idToken) { List idTokenAudiences = idToken.getAudience(); - Set oidcUserAudiences = new HashSet<>(existingOidcUser.getIdToken().getAudience()); - if (idTokenAudiences.size() != oidcUserAudiences.size()) { + List existingIdTokenAudiences = existingOidcUser.getIdToken().getAudience(); + if (idTokenAudiences == null || existingIdTokenAudiences == null + || idTokenAudiences.size() != existingIdTokenAudiences.size()) { return false; } for (String audience : idTokenAudiences) { - if (!oidcUserAudiences.contains(audience)) { + if (!existingIdTokenAudiences.contains(audience)) { return false; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java index 8a0c6b7aa3..efa91f6b82 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java @@ -24,9 +24,10 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.lang.Nullable; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; import org.springframework.security.oauth2.client.endpoint.RestClientRefreshTokenTokenResponseClient; @@ -51,7 +52,7 @@ public final class RefreshTokenOAuth2AuthorizedClientProvider private OAuth2AccessTokenResponseClient accessTokenResponseClient = new RestClientRefreshTokenTokenResponseClient(); - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; private Duration clockSkew = Duration.ofSeconds(60); @@ -78,8 +79,7 @@ public final class RefreshTokenOAuth2AuthorizedClientProvider * not supported */ @Override - @Nullable - public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { Assert.notNull(context, "context cannot be null"); OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient(); if (authorizedClient == null || authorizedClient.getRefreshToken() == null @@ -123,7 +123,8 @@ public final class RefreshTokenOAuth2AuthorizedClientProvider } private boolean hasTokenExpired(OAuth2Token token) { - return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + Instant expiresAt = token.getExpiresAt(); + return expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew)); } /** diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenReactiveOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenReactiveOAuth2AuthorizedClientProvider.java index a711b29559..809584d907 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenReactiveOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenReactiveOAuth2AuthorizedClientProvider.java @@ -180,7 +180,8 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider } private boolean hasTokenExpired(OAuth2Token token) { - return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + Instant expiresAt = token.getExpiresAt(); + return expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew)); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java index b98fd2e48a..aa1cd95d2e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeOAuth2AuthorizedClientProvider.java @@ -21,7 +21,8 @@ import java.time.Duration; import java.time.Instant; import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.RestClientTokenExchangeTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.TokenExchangeGrantRequest; @@ -45,9 +46,9 @@ public final class TokenExchangeOAuth2AuthorizedClientProvider implements OAuth2 private OAuth2AccessTokenResponseClient accessTokenResponseClient = new RestClientTokenExchangeTokenResponseClient(); - private Function subjectTokenResolver = this::resolveSubjectToken; + private Function subjectTokenResolver = this::resolveSubjectToken; - private Function actorTokenResolver = (context) -> null; + private Function actorTokenResolver = (context) -> null; private Duration clockSkew = Duration.ofSeconds(60); @@ -66,8 +67,7 @@ public final class TokenExchangeOAuth2AuthorizedClientProvider implements OAuth2 * supported */ @Override - @Nullable - public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { Assert.notNull(context, "context cannot be null"); ClientRegistration clientRegistration = context.getClientRegistration(); if (!AuthorizationGrantType.TOKEN_EXCHANGE.equals(clientRegistration.getAuthorizationGrantType())) { @@ -93,7 +93,7 @@ public final class TokenExchangeOAuth2AuthorizedClientProvider implements OAuth2 tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()); } - private OAuth2Token resolveSubjectToken(OAuth2AuthorizationContext context) { + private @Nullable OAuth2Token resolveSubjectToken(OAuth2AuthorizationContext context) { if (context.getPrincipal().getPrincipal() instanceof OAuth2Token accessToken) { return accessToken; } @@ -111,7 +111,8 @@ public final class TokenExchangeOAuth2AuthorizedClientProvider implements OAuth2 } private boolean hasTokenExpired(OAuth2Token token) { - return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + Instant expiresAt = token.getExpiresAt(); + return expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew)); } /** @@ -131,7 +132,8 @@ public final class TokenExchangeOAuth2AuthorizedClientProvider implements OAuth2 * @param subjectTokenResolver the resolver used for resolving the {@link OAuth2Token * subject token} */ - public void setSubjectTokenResolver(Function subjectTokenResolver) { + public void setSubjectTokenResolver( + Function subjectTokenResolver) { Assert.notNull(subjectTokenResolver, "subjectTokenResolver cannot be null"); this.subjectTokenResolver = subjectTokenResolver; } @@ -141,7 +143,7 @@ public final class TokenExchangeOAuth2AuthorizedClientProvider implements OAuth2 * @param actorTokenResolver the resolver used for resolving the {@link OAuth2Token * actor token} */ - public void setActorTokenResolver(Function actorTokenResolver) { + public void setActorTokenResolver(Function actorTokenResolver) { Assert.notNull(actorTokenResolver, "actorTokenResolver cannot be null"); this.actorTokenResolver = actorTokenResolver; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProvider.java index 6cdb564910..6dda08a461 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/TokenExchangeReactiveOAuth2AuthorizedClientProvider.java @@ -83,7 +83,7 @@ public final class TokenExchangeReactiveOAuth2AuthorizedClientProvider return this.subjectTokenResolver.apply(context) .flatMap((subjectToken) -> this.actorTokenResolver.apply(context) .map((actorToken) -> new TokenExchangeGrantRequest(clientRegistration, subjectToken, actorToken)) - .defaultIfEmpty(new TokenExchangeGrantRequest(clientRegistration, subjectToken, null))) + .switchIfEmpty(Mono.just(new TokenExchangeGrantRequest(clientRegistration, subjectToken, null)))) .flatMap(this.accessTokenResponseClient::getTokenResponse) .onErrorMap(OAuth2AuthorizationException.class, (ex) -> new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex)) @@ -94,14 +94,16 @@ public final class TokenExchangeReactiveOAuth2AuthorizedClientProvider private Mono resolveSubjectToken(OAuth2AuthorizationContext context) { // @formatter:off return Mono.just(context) - .map((ctx) -> ctx.getPrincipal().getPrincipal()) + .flatMap((ctx) -> Mono.justOrEmpty(ctx.getPrincipal()) + .flatMap((auth) -> Mono.justOrEmpty(auth.getPrincipal()))) .filter((principal) -> principal instanceof OAuth2Token) .cast(OAuth2Token.class); // @formatter:on } private boolean hasTokenExpired(OAuth2Token token) { - return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + Instant expiresAt = token.getExpiresAt(); + return expiresAt != null && this.clock.instant().isAfter(expiresAt.minus(this.clockSkew)); } /** diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/package-info.java new file mode 100644 index 0000000000..777c0ffbd1 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/annotation/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Annotations for OAuth2 Client (e.g. method parameters). + */ +@NullMarked +package org.springframework.security.oauth2.client.annotation; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/aot/hint/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/aot/hint/package-info.java new file mode 100644 index 0000000000..1dfa64584a --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/aot/hint/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Runtime hints for OAuth2 Client AOT support. + */ +@NullMarked +package org.springframework.security.oauth2.client.aot.hint; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index 1d060c7a2c..00f2a326b5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.client.authentication; +import java.util.Objects; + import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -74,11 +76,13 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange() .getAuthorizationResponse(); if (authorizationResponse.statusError()) { - throw new OAuth2AuthorizationException(authorizationResponse.getError()); + OAuth2Error error = authorizationResponse.getError(); + Assert.notNull(error, "error cannot be null when status is error"); + throw new OAuth2AuthorizationException(error); } OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange() .getAuthorizationRequest(); - if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + if (!Objects.equals(authorizationResponse.getState(), authorizationRequest.getState())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); throw new OAuth2AuthorizationException(oauth2Error); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java index 5b131f7665..2ec0a56e01 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java @@ -20,7 +20,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -50,9 +51,9 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti private OAuth2AuthorizationExchange authorizationExchange; - private OAuth2AccessToken accessToken; + private @Nullable OAuth2AccessToken accessToken; - private OAuth2RefreshToken refreshToken; + private @Nullable OAuth2RefreshToken refreshToken; /** * This constructor should be used when the Authorization Request/Response is @@ -97,7 +98,7 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, OAuth2AuthorizationExchange authorizationExchange, OAuth2AccessToken accessToken, - OAuth2RefreshToken refreshToken, Map additionalParameters) { + @Nullable OAuth2RefreshToken refreshToken, Map additionalParameters) { this(clientRegistration, authorizationExchange); Assert.notNull(accessToken, "accessToken cannot be null"); this.accessToken = accessToken; @@ -112,7 +113,7 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti } @Override - public Object getCredentials() { + public @Nullable Object getCredentials() { return (this.accessToken != null) ? this.accessToken.getTokenValue() : this.authorizationExchange.getAuthorizationResponse().getCode(); } @@ -137,7 +138,7 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti * Returns the {@link OAuth2AccessToken access token}. * @return the {@link OAuth2AccessToken} */ - public OAuth2AccessToken getAccessToken() { + public @Nullable OAuth2AccessToken getAccessToken() { return this.accessToken; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java index d8b23eed8b..053817766c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.authentication; +import java.util.Objects; import java.util.function.Function; import reactor.core.publisher.Mono; @@ -87,11 +88,13 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements Rea OAuth2AuthorizationResponse authorizationResponse = token.getAuthorizationExchange() .getAuthorizationResponse(); if (authorizationResponse.statusError()) { - return Mono.error(new OAuth2AuthorizationException(authorizationResponse.getError())); + OAuth2Error error = authorizationResponse.getError(); + Assert.notNull(error, "error cannot be null when status is error"); + return Mono.error(new OAuth2AuthorizationException(error)); } OAuth2AuthorizationRequest authorizationRequest = token.getAuthorizationExchange() .getAuthorizationRequest(); - if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + if (!Objects.equals(authorizationResponse.getState(), authorizationRequest.getState())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); return Mono.error(new OAuth2AuthorizationException(oauth2Error)); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java index 152e4fc297..b2db56fd75 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java @@ -21,6 +21,8 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -95,7 +97,7 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider } @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { + public @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication; // Section 3.1.2.1 Authentication Request - // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest scope @@ -120,9 +122,11 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); } OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken(); + Assert.notNull(accessToken, "accessToken cannot be null"); Map additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters(); OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest( loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters)); + Assert.notNull(oauth2User, "oauth2User cannot be null"); Collection authorities = new HashSet<>(oauth2User.getAuthorities()); Collection mappedAuthorities = new LinkedHashSet<>( this.authoritiesMapper.mapAuthorities(authorities)); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java index 5067193d3d..c809db5a6d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationToken.java @@ -19,7 +19,8 @@ package org.springframework.security.oauth2.client.authentication; import java.util.Collection; import java.util.Collections; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -47,15 +48,15 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken private static final long serialVersionUID = 620L; - private OAuth2User principal; + private @Nullable OAuth2User principal; private ClientRegistration clientRegistration; private OAuth2AuthorizationExchange authorizationExchange; - private OAuth2AccessToken accessToken; + private @Nullable OAuth2AccessToken accessToken; - private OAuth2RefreshToken refreshToken; + private @Nullable OAuth2RefreshToken refreshToken; /** * This constructor should be used when the Authorization Request/Response is @@ -118,7 +119,7 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken } @Override - public OAuth2User getPrincipal() { + public @Nullable OAuth2User getPrincipal() { return this.principal; } @@ -147,7 +148,7 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken * Returns the {@link OAuth2AccessToken access token}. * @return the {@link OAuth2AccessToken} */ - public OAuth2AccessToken getAccessToken() { + public @Nullable OAuth2AccessToken getAccessToken() { return this.accessToken; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java index 7c9833b261..fc213cc9c7 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java @@ -120,6 +120,7 @@ public class OAuth2LoginReactiveAuthenticationManager implements ReactiveAuthent private Mono onSuccess(OAuth2AuthorizationCodeAuthenticationToken authentication) { OAuth2AccessToken accessToken = authentication.getAccessToken(); + Assert.notNull(accessToken, "accessToken cannot be null"); Map additionalParameters = authentication.getAdditionalParameters(); OAuth2UserRequest userRequest = new OAuth2UserRequest(authentication.getClientRegistration(), accessToken, additionalParameters); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/package-info.java index cde948572c..19e32cd1b5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/package-info.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/package-info.java @@ -18,4 +18,7 @@ * Support classes and interfaces for authenticating and authorizing a client with an * OAuth 2.0 Authorization Server using a specific authorization grant flow. */ +@NullMarked package org.springframework.security.oauth2.client.authentication; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultOAuth2TokenRequestParametersConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultOAuth2TokenRequestParametersConverter.java index 5329796f7d..39840fab99 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultOAuth2TokenRequestParametersConverter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultOAuth2TokenRequestParametersConverter.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.client.endpoint; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; @@ -58,7 +60,7 @@ import org.springframework.util.MultiValueMap; public final class DefaultOAuth2TokenRequestParametersConverter implements Converter> { - private final Converter> defaultParametersConverter = createDefaultParametersConverter(); + private final Converter> defaultParametersConverter = createDefaultParametersConverter(); @Override public MultiValueMap convert(T grantRequest) { @@ -81,7 +83,7 @@ public final class DefaultOAuth2TokenRequestParametersConverter Converter> createDefaultParametersConverter() { + private static Converter> createDefaultParametersConverter() { return (grantRequest) -> { if (grantRequest instanceof OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) { return OAuth2AuthorizationCodeGrantRequest.defaultParameters(authorizationCodeGrantRequest); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverter.java index 64258a39d7..4dc44cc2db 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverter.java @@ -31,6 +31,7 @@ import com.nimbusds.jose.jwk.KeyType; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -77,7 +78,7 @@ import org.springframework.util.MultiValueMap; * JOSE + JWT SDK */ public final class NimbusJwtClientAuthenticationParametersConverter - implements Converter> { + implements Converter> { private static final String INVALID_KEY_ERROR_CODE = "invalid_key"; @@ -104,7 +105,7 @@ public final class NimbusJwtClientAuthenticationParametersConverter convert(T authorizationGrantRequest) { + public @Nullable MultiValueMap convert(T authorizationGrantRequest) { Assert.notNull(authorizationGrantRequest, "authorizationGrantRequest cannot be null"); ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration(); @@ -173,7 +174,7 @@ public final class NimbusJwtClientAuthenticationParametersConverter scopes = JsonNodeUtils.findValue(clientRegistrationNode, "scopes", JsonNodeUtils.STRING_SET, + context); + String clientName = JsonNodeUtils.findStringValue(clientRegistrationNode, "clientName"); + String authorizationUri = JsonNodeUtils.findStringValue(providerDetailsNode, "authorizationUri"); + String tokenUri = JsonNodeUtils.findStringValue(providerDetailsNode, "tokenUri"); + Assert.hasText(tokenUri, "tokenUri cannot be null or empty"); + String userInfoUri = JsonNodeUtils.findStringValue(userInfoEndpointNode, "uri"); + String userNameAttributeName = JsonNodeUtils.findStringValue(userInfoEndpointNode, "userNameAttributeName"); + String jwkSetUri = JsonNodeUtils.findStringValue(providerDetailsNode, "jwkSetUri"); + String issuerUri = JsonNodeUtils.findStringValue(providerDetailsNode, "issuerUri"); + Map configurationMetadata = JsonNodeUtils.findValue(providerDetailsNode, + "configurationMetadata", JsonNodeUtils.STRING_OBJECT_MAP, context); + ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(registrationId) + .clientId(clientId) + .clientSecret(clientSecret) .clientAuthenticationMethod(CLIENT_AUTHENTICATION_METHOD_CONVERTER .convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "clientAuthenticationMethod"))) .authorizationGrantType(AUTHORIZATION_GRANT_TYPE_CONVERTER .convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "authorizationGrantType"))) - .redirectUri(JsonNodeUtils.findStringValue(clientRegistrationNode, "redirectUri")) - .scope(JsonNodeUtils.findValue(clientRegistrationNode, "scopes", JsonNodeUtils.STRING_SET, context)) - .clientName(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientName")) - .authorizationUri(JsonNodeUtils.findStringValue(providerDetailsNode, "authorizationUri")) - .tokenUri(JsonNodeUtils.findStringValue(providerDetailsNode, "tokenUri")) - .userInfoUri(JsonNodeUtils.findStringValue(userInfoEndpointNode, "uri")) + .redirectUri(redirectUri) + .scope(scopes) + .clientName(clientName) + .authorizationUri(authorizationUri) + .tokenUri(tokenUri) + .userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AUTHENTICATION_METHOD_CONVERTER .convert(JsonNodeUtils.findObjectNode(userInfoEndpointNode, "authenticationMethod"))) - .userNameAttributeName(JsonNodeUtils.findStringValue(userInfoEndpointNode, "userNameAttributeName")) - .jwkSetUri(JsonNodeUtils.findStringValue(providerDetailsNode, "jwkSetUri")) - .issuerUri(JsonNodeUtils.findStringValue(providerDetailsNode, "issuerUri")) - .providerConfigurationMetadata(JsonNodeUtils.findValue(providerDetailsNode, "configurationMetadata", - JsonNodeUtils.STRING_OBJECT_MAP, context)) - .build(); + .userNameAttributeName(userNameAttributeName) + .jwkSetUri(jwkSetUri) + .issuerUri(issuerUri) + .providerConfigurationMetadata( + (configurationMetadata != null) ? configurationMetadata : java.util.Collections.emptyMap()); + return builder.build(); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/JsonNodeUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/JsonNodeUtils.java index 4f181aabb1..aa86aef6bf 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/JsonNodeUtils.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/JsonNodeUtils.java @@ -19,6 +19,7 @@ package org.springframework.security.oauth2.client.jackson; import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import tools.jackson.core.type.TypeReference; import tools.jackson.databind.DeserializationContext; import tools.jackson.databind.JsonNode; @@ -38,7 +39,7 @@ abstract class JsonNodeUtils { static final TypeReference> STRING_OBJECT_MAP = new TypeReference<>() { }; - static String findStringValue(JsonNode jsonNode, String fieldName) { + static @Nullable String findStringValue(@Nullable JsonNode jsonNode, String fieldName) { if (jsonNode == null) { return null; } @@ -46,7 +47,7 @@ abstract class JsonNodeUtils { return (value != null && value.isString()) ? value.stringValue() : null; } - static T findValue(JsonNode jsonNode, String fieldName, TypeReference valueTypeReference, + static @Nullable T findValue(@Nullable JsonNode jsonNode, String fieldName, TypeReference valueTypeReference, DeserializationContext context) { if (jsonNode == null) { return null; @@ -56,7 +57,7 @@ abstract class JsonNodeUtils { ? context.readTreeAsValue(value, context.getTypeFactory().constructType(valueTypeReference)) : null; } - static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) { + static @Nullable JsonNode findObjectNode(@Nullable JsonNode jsonNode, String fieldName) { if (jsonNode == null) { return null; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestDeserializer.java index 786207e68e..7bf567ff5a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestDeserializer.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestDeserializer.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.client.jackson; +import java.util.Map; + import tools.jackson.core.JsonParser; import tools.jackson.core.exc.StreamReadException; import tools.jackson.databind.DeserializationContext; @@ -26,6 +28,7 @@ import tools.jackson.databind.util.StdConverter; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest.Builder; +import org.springframework.util.Assert; /** * A {@code JsonDeserializer} for {@link OAuth2AuthorizationRequest}. @@ -50,15 +53,25 @@ final class OAuth2AuthorizationRequestDeserializer extends ValueDeserializer additionalParameters = JsonNodeUtils.findValue(root, "additionalParameters", + JsonNodeUtils.STRING_OBJECT_MAP, context); builder.additionalParameters( - JsonNodeUtils.findValue(root, "additionalParameters", JsonNodeUtils.STRING_OBJECT_MAP, context)); - builder.authorizationRequestUri(JsonNodeUtils.findStringValue(root, "authorizationRequestUri")); - builder.attributes(JsonNodeUtils.findValue(root, "attributes", JsonNodeUtils.STRING_OBJECT_MAP, context)); + (additionalParameters != null) ? additionalParameters : java.util.Collections.emptyMap()); + String authorizationRequestUri = JsonNodeUtils.findStringValue(root, "authorizationRequestUri"); + Assert.hasText(authorizationRequestUri, "authorizationRequestUri cannot be null or empty"); + builder.authorizationRequestUri(authorizationRequestUri); + Map attributes = JsonNodeUtils.findValue(root, "attributes", JsonNodeUtils.STRING_OBJECT_MAP, + context); + builder.attributes((attributes != null) ? attributes : java.util.Collections.emptyMap()); return builder.build(); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/StdConverters.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/StdConverters.java index c9fafcdd6e..6bff5f06a5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/StdConverters.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/StdConverters.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.jackson; +import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; import tools.jackson.databind.util.StdConverter; @@ -23,6 +24,7 @@ import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.Assert; /** * {@code StdConverter} implementations. @@ -36,9 +38,9 @@ abstract class StdConverters { static final class AccessTokenTypeConverter extends StdConverter { @Override - public OAuth2AccessToken.TokenType convert(JsonNode jsonNode) { + public OAuth2AccessToken.@Nullable TokenType convert(JsonNode jsonNode) { String value = JsonNodeUtils.findStringValue(jsonNode, "value"); - if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(value)) { + if (value != null && OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(value)) { return OAuth2AccessToken.TokenType.BEARER; } return null; @@ -51,6 +53,7 @@ abstract class StdConverters { @Override public ClientAuthenticationMethod convert(JsonNode jsonNode) { String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + Assert.hasText(value, "value cannot be null or empty"); return ClientAuthenticationMethod.valueOf(value); } @@ -61,6 +64,7 @@ abstract class StdConverters { @Override public AuthorizationGrantType convert(JsonNode jsonNode) { String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + Assert.hasText(value, "value cannot be null or empty"); if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equalsIgnoreCase(value)) { return AuthorizationGrantType.AUTHORIZATION_CODE; } @@ -75,15 +79,15 @@ abstract class StdConverters { static final class AuthenticationMethodConverter extends StdConverter { @Override - public AuthenticationMethod convert(JsonNode jsonNode) { + public @Nullable AuthenticationMethod convert(JsonNode jsonNode) { String value = JsonNodeUtils.findStringValue(jsonNode, "value"); - if (AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) { + if (value != null && AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) { return AuthenticationMethod.HEADER; } - if (AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) { + if (value != null && AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) { return AuthenticationMethod.FORM; } - if (AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) { + if (value != null && AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) { return AuthenticationMethod.QUERY; } return null; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/package-info.java index 3ac4da65bb..4d97e97b53 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/package-info.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/package-info.java @@ -17,4 +17,7 @@ /** * Jackson 3+ serialization support for OAuth2 client. */ +@NullMarked package org.springframework.security.oauth2.client.jackson; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java index d9b8eedaf4..36c9d415d2 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java @@ -17,6 +17,8 @@ package org.springframework.security.oauth2.client.jackson2; import java.io.IOException; +import java.util.Map; +import java.util.Set; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; @@ -29,6 +31,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.util.Assert; /** * A {@code JsonDeserializer} for {@link ClientRegistration}. @@ -56,28 +59,44 @@ final class ClientRegistrationDeserializer extends JsonDeserializer scopes = JsonNodeUtils.findValue(clientRegistrationNode, "scopes", JsonNodeUtils.STRING_SET, + mapper); + String clientName = JsonNodeUtils.findStringValue(clientRegistrationNode, "clientName"); + String authorizationUri = JsonNodeUtils.findStringValue(providerDetailsNode, "authorizationUri"); + String tokenUri = JsonNodeUtils.findStringValue(providerDetailsNode, "tokenUri"); + Assert.hasText(tokenUri, "tokenUri cannot be empty"); + String userInfoUri = JsonNodeUtils.findStringValue(userInfoEndpointNode, "uri"); + String userNameAttributeName = JsonNodeUtils.findStringValue(userInfoEndpointNode, "userNameAttributeName"); + String jwkSetUri = JsonNodeUtils.findStringValue(providerDetailsNode, "jwkSetUri"); + String issuerUri = JsonNodeUtils.findStringValue(providerDetailsNode, "issuerUri"); + Map configurationMetadata = JsonNodeUtils.findValue(providerDetailsNode, + "configurationMetadata", JsonNodeUtils.STRING_OBJECT_MAP, mapper); + ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(registrationId) + .clientId(clientId) + .clientSecret(clientSecret) .clientAuthenticationMethod(CLIENT_AUTHENTICATION_METHOD_CONVERTER .convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "clientAuthenticationMethod"))) .authorizationGrantType(AUTHORIZATION_GRANT_TYPE_CONVERTER .convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "authorizationGrantType"))) - .redirectUri(JsonNodeUtils.findStringValue(clientRegistrationNode, "redirectUri")) - .scope(JsonNodeUtils.findValue(clientRegistrationNode, "scopes", JsonNodeUtils.STRING_SET, mapper)) - .clientName(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientName")) - .authorizationUri(JsonNodeUtils.findStringValue(providerDetailsNode, "authorizationUri")) - .tokenUri(JsonNodeUtils.findStringValue(providerDetailsNode, "tokenUri")) - .userInfoUri(JsonNodeUtils.findStringValue(userInfoEndpointNode, "uri")) + .redirectUri(redirectUri) + .scope(scopes) + .clientName(clientName) + .authorizationUri(authorizationUri) + .tokenUri(tokenUri) + .userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AUTHENTICATION_METHOD_CONVERTER .convert(JsonNodeUtils.findObjectNode(userInfoEndpointNode, "authenticationMethod"))) - .userNameAttributeName(JsonNodeUtils.findStringValue(userInfoEndpointNode, "userNameAttributeName")) - .jwkSetUri(JsonNodeUtils.findStringValue(providerDetailsNode, "jwkSetUri")) - .issuerUri(JsonNodeUtils.findStringValue(providerDetailsNode, "issuerUri")) - .providerConfigurationMetadata(JsonNodeUtils.findValue(providerDetailsNode, "configurationMetadata", - JsonNodeUtils.STRING_OBJECT_MAP, mapper)) - .build(); + .userNameAttributeName(userNameAttributeName) + .jwkSetUri(jwkSetUri) + .issuerUri(issuerUri) + .providerConfigurationMetadata(configurationMetadata); + return builder.build(); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java index 1bfdbfa21a..7c11002ecd 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java @@ -22,6 +22,7 @@ import java.util.Set; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.jspecify.annotations.Nullable; /** * Utility class for {@code JsonNode}. @@ -41,7 +42,7 @@ abstract class JsonNodeUtils { static final TypeReference> STRING_OBJECT_MAP = new TypeReference<>() { }; - static String findStringValue(JsonNode jsonNode, String fieldName) { + static @Nullable String findStringValue(@Nullable JsonNode jsonNode, String fieldName) { if (jsonNode == null) { return null; } @@ -49,7 +50,7 @@ abstract class JsonNodeUtils { return (value != null && value.isTextual()) ? value.asText() : null; } - static T findValue(JsonNode jsonNode, String fieldName, TypeReference valueTypeReference, + static @Nullable T findValue(@Nullable JsonNode jsonNode, String fieldName, TypeReference valueTypeReference, ObjectMapper mapper) { if (jsonNode == null) { return null; @@ -58,7 +59,7 @@ abstract class JsonNodeUtils { return (value != null && value.isContainerNode()) ? mapper.convertValue(value, valueTypeReference) : null; } - static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) { + static @Nullable JsonNode findObjectNode(@Nullable JsonNode jsonNode, String fieldName) { if (jsonNode == null) { return null; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestDeserializer.java index 1296a5b783..8f71c78b4d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestDeserializer.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestDeserializer.java @@ -17,6 +17,8 @@ package org.springframework.security.oauth2.client.jackson2; import java.io.IOException; +import java.util.Collections; +import java.util.Map; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; @@ -29,6 +31,7 @@ import com.fasterxml.jackson.databind.util.StdConverter; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest.Builder; +import org.springframework.util.Assert; /** * A {@code JsonDeserializer} for {@link OAuth2AuthorizationRequest}. @@ -59,15 +62,25 @@ final class OAuth2AuthorizationRequestDeserializer extends JsonDeserializer additionalParameters = JsonNodeUtils.findValue(root, "additionalParameters", + JsonNodeUtils.STRING_OBJECT_MAP, mapper); + builder.additionalParameters((additionalParameters != null) ? additionalParameters : Collections.emptyMap()); + String authorizationRequestUri = JsonNodeUtils.findStringValue(root, "authorizationRequestUri"); + if (authorizationRequestUri != null) { + builder.authorizationRequestUri(authorizationRequestUri); + } + Map attributes = JsonNodeUtils.findValue(root, "attributes", JsonNodeUtils.STRING_OBJECT_MAP, + mapper); + builder.attributes((attributes != null) ? attributes : Collections.emptyMap()); return builder.build(); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java index 2bceb429e3..0b698c7614 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/StdConverters.java @@ -18,11 +18,13 @@ package org.springframework.security.oauth2.client.jackson2; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.util.StdConverter; +import org.jspecify.annotations.Nullable; import org.springframework.security.oauth2.core.AuthenticationMethod; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.util.Assert; /** * {@code StdConverter} implementations. @@ -39,9 +41,9 @@ abstract class StdConverters { static final class AccessTokenTypeConverter extends StdConverter { @Override - public OAuth2AccessToken.TokenType convert(JsonNode jsonNode) { + public OAuth2AccessToken.@Nullable TokenType convert(JsonNode jsonNode) { String value = JsonNodeUtils.findStringValue(jsonNode, "value"); - if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(value)) { + if (value != null && OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(value)) { return OAuth2AccessToken.TokenType.BEARER; } return null; @@ -54,6 +56,7 @@ abstract class StdConverters { @Override public ClientAuthenticationMethod convert(JsonNode jsonNode) { String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + Assert.hasText(value, "value cannot be null or empty"); return ClientAuthenticationMethod.valueOf(value); } @@ -64,6 +67,7 @@ abstract class StdConverters { @Override public AuthorizationGrantType convert(JsonNode jsonNode) { String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + Assert.hasText(value, "value cannot be null or empty"); if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equalsIgnoreCase(value)) { return AuthorizationGrantType.AUTHORIZATION_CODE; } @@ -78,15 +82,15 @@ abstract class StdConverters { static final class AuthenticationMethodConverter extends StdConverter { @Override - public AuthenticationMethod convert(JsonNode jsonNode) { + public @Nullable AuthenticationMethod convert(JsonNode jsonNode) { String value = JsonNodeUtils.findStringValue(jsonNode, "value"); - if (AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) { + if (value != null && AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) { return AuthenticationMethod.HEADER; } - if (AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) { + if (value != null && AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) { return AuthenticationMethod.FORM; } - if (AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) { + if (value != null && AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) { return AuthenticationMethod.QUERY; } return null; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/package-info.java index 5477db1b36..3ba7cc599c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/package-info.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/package-info.java @@ -17,4 +17,7 @@ /** * Jackson 2 serialization support for OAuth2 client. */ +@NullMarked package org.springframework.security.oauth2.client.jackson2; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java index 8fa7d34d63..647bd1d5a3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java @@ -22,6 +22,9 @@ import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Collection; import java.util.Map; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; @@ -117,7 +120,7 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati } @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { + public @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication; // Section 3.1.2.1 Authentication Request - // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest @@ -136,10 +139,11 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange() .getAuthorizationResponse(); if (authorizationResponse.statusError()) { - throw new OAuth2AuthenticationException(authorizationResponse.getError(), - authorizationResponse.getError().toString()); + OAuth2Error error = authorizationResponse.getError(); + Assert.notNull(error, "error cannot be null when status is error"); + throw new OAuth2AuthenticationException(error, error.toString()); } - if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + if (!Objects.equals(authorizationResponse.getState(), authorizationRequest.getState())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } @@ -157,6 +161,7 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati validateNonce(authorizationRequest, idToken); OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration, accessTokenResponse.getAccessToken(), idToken, additionalParameters)); + Assert.notNull(oidcUser, "oidcUser cannot be null"); Collection mappedAuthorities = this.authoritiesMapper .mapAuthorities(oidcUser.getAuthorities()); OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken( @@ -244,7 +249,9 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwtDecoder) { try { Map parameters = accessTokenResponse.getAdditionalParameters(); - return jwtDecoder.decode((String) parameters.get(OidcParameterNames.ID_TOKEN)); + String idToken = (String) parameters.get(OidcParameterNames.ID_TOKEN); + Assert.hasText(idToken, "id_token parameter cannot be null or empty"); + return jwtDecoder.decode(idToken); } catch (JwtException ex) { OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java index 437b17f7f0..f2f6760484 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java @@ -22,6 +22,7 @@ import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Collection; import java.util.Map; +import java.util.Objects; import reactor.core.publisher.Mono; @@ -132,10 +133,11 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements React .getAuthorizationExchange() .getAuthorizationResponse(); if (authorizationResponse.statusError()) { - return Mono.error(new OAuth2AuthenticationException(authorizationResponse.getError(), - authorizationResponse.getError().toString())); + OAuth2Error error = authorizationResponse.getError(); + Assert.notNull(error, "error cannot be null when status is error"); + return Mono.error(new OAuth2AuthenticationException(error, error.toString())); } - if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + if (!Objects.equals(authorizationResponse.getState(), authorizationRequest.getState())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); return Mono.error(new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString())); } @@ -213,6 +215,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements React OAuth2AccessTokenResponse accessTokenResponse) { ReactiveJwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration); String rawIdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN); + Assert.hasText(rawIdToken, "id_token parameter cannot be null or empty"); // @formatter:off return jwtDecoder.decode(rawIdToken) .map((jwt) -> diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListener.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListener.java index 1e983522e9..36d7928e9d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListener.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListener.java @@ -16,12 +16,15 @@ package org.springframework.security.oauth2.client.oidc.authentication; +import java.net.URL; import java.time.Duration; +import java.time.Instant; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -82,7 +85,7 @@ public final class OidcAuthorizedClientRefreshedEventListener private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; private Duration clockSkew = Duration.ofSeconds(60); @@ -131,6 +134,7 @@ public final class OidcAuthorizedClientRefreshedEventListener OidcUserRequest userRequest = new OidcUserRequest(clientRegistration, accessTokenResponse.getAccessToken(), idToken, additionalParameters); OidcUser oidcUser = this.userService.loadUser(userRequest); + Assert.notNull(oidcUser, "oidcUser cannot be null"); Collection mappedAuthorities = this.authoritiesMapper .mapAuthorities(oidcUser.getAuthorities()); OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(oidcUser, mappedAuthorities, @@ -216,7 +220,9 @@ public final class OidcAuthorizedClientRefreshedEventListener private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwtDecoder) { try { Map parameters = accessTokenResponse.getAdditionalParameters(); - return jwtDecoder.decode((String) parameters.get(OidcParameterNames.ID_TOKEN)); + String idToken = (String) parameters.get(OidcParameterNames.ID_TOKEN); + Assert.hasText(idToken, "id_token parameter cannot be null or empty"); + return jwtDecoder.decode(idToken); } catch (JwtException ex) { OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null); @@ -250,7 +256,10 @@ public final class OidcAuthorizedClientRefreshedEventListener } private void validateIssuer(OidcUser existingOidcUser, OidcIdToken idToken) { - if (!idToken.getIssuer().toString().equals(existingOidcUser.getIdToken().getIssuer().toString())) { + URL idTokenIssuer = idToken.getIssuer(); + URL existingIdTokenIssuer = existingOidcUser.getIdToken().getIssuer(); + if (idTokenIssuer == null || existingIdTokenIssuer == null + || !idTokenIssuer.toString().equals(existingIdTokenIssuer.toString())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issuer", REFRESH_TOKEN_RESPONSE_ERROR_URI); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); @@ -258,7 +267,10 @@ public final class OidcAuthorizedClientRefreshedEventListener } private void validateSubject(OidcUser existingOidcUser, OidcIdToken idToken) { - if (!idToken.getSubject().equals(existingOidcUser.getIdToken().getSubject())) { + String idTokenSubject = idToken.getSubject(); + String existingIdTokenSubject = existingOidcUser.getIdToken().getSubject(); + if (idTokenSubject == null || existingIdTokenSubject == null + || !idTokenSubject.equals(existingIdTokenSubject)) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid subject", REFRESH_TOKEN_RESPONSE_ERROR_URI); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); @@ -266,7 +278,10 @@ public final class OidcAuthorizedClientRefreshedEventListener } private void validateIssuedAt(OidcUser existingOidcUser, OidcIdToken idToken) { - if (!idToken.getIssuedAt().isAfter(existingOidcUser.getIdToken().getIssuedAt().minus(this.clockSkew))) { + Instant idTokenIssuedAt = idToken.getIssuedAt(); + Instant existingIdTokenIssuedAt = existingOidcUser.getIdToken().getIssuedAt(); + if (idTokenIssuedAt == null || existingIdTokenIssuedAt == null + || !idTokenIssuedAt.isAfter(existingIdTokenIssuedAt.minus(this.clockSkew))) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issued at time", REFRESH_TOKEN_RESPONSE_ERROR_URI); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); @@ -283,12 +298,13 @@ public final class OidcAuthorizedClientRefreshedEventListener private boolean isValidAudience(OidcUser existingOidcUser, OidcIdToken idToken) { List idTokenAudiences = idToken.getAudience(); - Set oidcUserAudiences = new HashSet<>(existingOidcUser.getIdToken().getAudience()); - if (idTokenAudiences.size() != oidcUserAudiences.size()) { + List existingIdTokenAudiences = existingOidcUser.getIdToken().getAudience(); + if (idTokenAudiences == null || existingIdTokenAudiences == null + || idTokenAudiences.size() != existingIdTokenAudiences.size()) { return false; } for (String audience : idTokenAudiences) { - if (!oidcUserAudiences.contains(audience)) { + if (!existingIdTokenAudiences.contains(audience)) { return false; } } @@ -300,7 +316,7 @@ public final class OidcAuthorizedClientRefreshedEventListener return; } - if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) { + if (!Objects.equals(idToken.getAuthenticatedAt(), existingOidcUser.getIdToken().getAuthenticatedAt())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time", REFRESH_TOKEN_RESPONSE_ERROR_URI); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); @@ -312,7 +328,7 @@ public final class OidcAuthorizedClientRefreshedEventListener return; } - if (!idToken.getNonce().equals(existingOidcUser.getIdToken().getNonce())) { + if (!Objects.equals(idToken.getNonce(), existingOidcUser.getIdToken().getNonce())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE, "Invalid nonce", REFRESH_TOKEN_RESPONSE_ERROR_URI); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java index 6b52c5d5c1..fdfcd6416d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java @@ -76,8 +76,9 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator { // during Discovery) // MUST exactly match the value of the iss (issuer) Claim. String metadataIssuer = this.clientRegistration.getProviderDetails().getIssuerUri(); - if (metadataIssuer != null && !Objects.equals(metadataIssuer, idToken.getIssuer().toExternalForm())) { - invalidClaims.put(IdTokenClaimNames.ISS, idToken.getIssuer()); + URL issuer = idToken.getIssuer(); + if (metadataIssuer != null && issuer != null && !Objects.equals(metadataIssuer, issuer.toExternalForm())) { + invalidClaims.put(IdTokenClaimNames.ISS, issuer); } // 3. The Client MUST validate that the aud (audience) Claim contains its // client_id value @@ -86,13 +87,14 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator { // The ID Token MUST be rejected if the ID Token does not list the Client as a // valid audience, // or if it contains additional audiences not trusted by the Client. - if (!idToken.getAudience().contains(this.clientRegistration.getClientId())) { - invalidClaims.put(IdTokenClaimNames.AUD, idToken.getAudience()); + List audience = idToken.getAudience(); + if (audience == null || !audience.contains(this.clientRegistration.getClientId())) { + invalidClaims.put(IdTokenClaimNames.AUD, audience); } // 4. If the ID Token contains multiple audiences, // the Client SHOULD verify that an azp Claim is present. String authorizedParty = idToken.getClaimAsString(IdTokenClaimNames.AZP); - if (idToken.getAudience().size() > 1 && authorizedParty == null) { + if (audience != null && audience.size() > 1 && authorizedParty == null) { invalidClaims.put(IdTokenClaimNames.AZP, authorizedParty); } // 5. If an azp (authorized party) Claim is present, @@ -106,15 +108,17 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator { // TODO Depends on gh-4413 // 9. The current time MUST be before the time represented by the exp Claim. Instant now = Instant.now(this.clock); - if (now.minus(this.clockSkew).isAfter(idToken.getExpiresAt())) { - invalidClaims.put(IdTokenClaimNames.EXP, idToken.getExpiresAt()); + Instant expiresAt = idToken.getExpiresAt(); + if (expiresAt != null && now.minus(this.clockSkew).isAfter(expiresAt)) { + invalidClaims.put(IdTokenClaimNames.EXP, expiresAt); } // 10. The iat Claim can be used to reject tokens that were issued too far away // from the current time, // limiting the amount of time that nonces need to be stored to prevent attacks. // The acceptable range is Client specific. - if (now.plus(this.clockSkew).isBefore(idToken.getIssuedAt())) { - invalidClaims.put(IdTokenClaimNames.IAT, idToken.getIssuedAt()); + Instant issuedAt = idToken.getIssuedAt(); + if (issuedAt != null && now.plus(this.clockSkew).isBefore(issuedAt)) { + invalidClaims.put(IdTokenClaimNames.IAT, issuedAt); } if (!invalidClaims.isEmpty()) { return OAuth2TokenValidatorResult.failure(invalidIdToken(invalidClaims)); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/event/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/event/package-info.java new file mode 100644 index 0000000000..2aa4033bbc --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/event/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Events for OpenID Connect 1.0 authentication. + */ +@NullMarked +package org.springframework.security.oauth2.client.oidc.authentication.event; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimAccessor.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimAccessor.java index 0f7f36f275..4c654ecb4c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimAccessor.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/LogoutTokenClaimAccessor.java @@ -21,7 +21,10 @@ import java.time.Instant; import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.util.Assert; /** * A {@link ClaimAccessor} for the "claims" that can be returned in OIDC Logout @@ -41,14 +44,16 @@ public interface LogoutTokenClaimAccessor extends ClaimAccessor { * @return the Issuer identifier */ default URL getIssuer() { - return this.getClaimAsURL(LogoutTokenClaimNames.ISS); + URL issuer = this.getClaimAsURL(LogoutTokenClaimNames.ISS); + Assert.notNull(issuer, "issuer cannot be null"); + return issuer; } /** * Returns the Subject identifier {@code (sub)}. * @return the Subject identifier */ - default String getSubject() { + default @Nullable String getSubject() { return this.getClaimAsString(LogoutTokenClaimNames.SUB); } @@ -57,14 +62,16 @@ public interface LogoutTokenClaimAccessor extends ClaimAccessor { * @return the Audience(s) that this ID Token is intended for */ default List getAudience() { - return this.getClaimAsStringList(LogoutTokenClaimNames.AUD); + List audience = this.getClaimAsStringList(LogoutTokenClaimNames.AUD); + Assert.notNull(audience, "audience cannot be null"); + return audience; } /** * Returns the time at which the ID Token was issued {@code (iat)}. * @return the time at which the ID Token was issued */ - default Instant getIssuedAt() { + default @Nullable Instant getIssuedAt() { return this.getClaimAsInstant(LogoutTokenClaimNames.IAT); } @@ -73,14 +80,16 @@ public interface LogoutTokenClaimAccessor extends ClaimAccessor { * @return the identifying {@link Map} */ default Map getEvents() { - return getClaimAsMap(LogoutTokenClaimNames.EVENTS); + Map events = getClaimAsMap(LogoutTokenClaimNames.EVENTS); + Assert.notNull(events, "events cannot be null"); + return events; } /** * Returns a {@code String} value {@code (sid)} representing the OIDC Provider session * @return the value representing the OIDC Provider session */ - default String getSessionId() { + default @Nullable String getSessionId() { return getClaimAsString(LogoutTokenClaimNames.SID); } @@ -90,7 +99,9 @@ public interface LogoutTokenClaimAccessor extends ClaimAccessor { * @return the JWT ID claim which provides a unique identifier for the JWT */ default String getId() { - return this.getClaimAsString(LogoutTokenClaimNames.JTI); + String jti = this.getClaimAsString(LogoutTokenClaimNames.JTI); + Assert.hasText(jti, "jti cannot be empty"); + return jti; } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutToken.java index 1dfff5ede7..e7000ce628 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutToken.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutToken.java @@ -24,6 +24,8 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.AbstractOAuth2Token; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.util.Assert; @@ -59,10 +61,10 @@ public class OidcLogoutToken extends AbstractOAuth2Token implements LogoutTokenC * @param issuedAt the time at which the Logout Token was issued {@code (iat)} * @param claims the claims about the logout statement */ - OidcLogoutToken(String tokenValue, Instant issuedAt, Map claims) { + OidcLogoutToken(String tokenValue, @Nullable Instant issuedAt, Map claims) { super(tokenValue, issuedAt, Instant.MAX); - this.claims = Collections.unmodifiableMap(claims); Assert.notNull(claims, "claims must not be null"); + this.claims = Collections.unmodifiableMap(claims); } @Override @@ -201,7 +203,8 @@ public class OidcLogoutToken extends AbstractOAuth2Token implements LogoutTokenC "logout token must contain an events claim that contains a member called " + "'" + BACKCHANNEL_LOGOUT_TOKEN_EVENT_NAME + "' whose value is an empty Map"); Assert.isNull(this.claims.get("nonce"), "logout token must not contain a nonce claim"); - Instant iat = toInstant(this.claims.get(IdTokenClaimNames.IAT)); + Object iatClaim = this.claims.get(IdTokenClaimNames.IAT); + Instant iat = (iatClaim != null) ? toInstant(iatClaim) : null; return new OidcLogoutToken(this.tokenValue, iat, this.claims); } @@ -215,7 +218,7 @@ public class OidcLogoutToken extends AbstractOAuth2Token implements LogoutTokenC return object.isEmpty(); } - private Instant toInstant(Object timestamp) { + private @Nullable Instant toInstant(@Nullable Object timestamp) { if (timestamp != null) { Assert.isInstanceOf(Instant.class, timestamp, "timestamps must be of type Instant"); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/package-info.java new file mode 100644 index 0000000000..d0183ad0f9 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/logout/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Support for OpenID Connect 1.0 Logout. + */ +@NullMarked +package org.springframework.security.oauth2.client.oidc.authentication.logout; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/package-info.java index 43383f019f..1c67ea4afc 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/package-info.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/package-info.java @@ -18,4 +18,7 @@ * Support classes and interfaces for authenticating and authorizing a client with an * OpenID Connect 1.0 Provider using a specific authorization grant flow. */ +@NullMarked package org.springframework.security.oauth2.client.oidc.authentication; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/package-info.java new file mode 100644 index 0000000000..8fd7b78fc9 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/server/session/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Reactive support for OpenID Connect 1.0 Session Management. + */ +@NullMarked +package org.springframework.security.oauth2.client.oidc.server.session; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistry.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistry.java index e00360623a..13da5be113 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistry.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistry.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.oidc.session; +import java.net.URL; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -26,6 +27,7 @@ import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.security.oauth2.client.oidc.authentication.logout.LogoutTokenClaimNames; import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken; @@ -96,7 +98,11 @@ public final class InMemoryOidcSessionRegistry implements OidcSessionRegistry { String sessionId) { return (session) -> { List thatAudience = session.getPrincipal().getAudience(); - String thatIssuer = session.getPrincipal().getIssuer().toString(); + URL thatIssuerUrl = session.getPrincipal().getIssuer(); + if (thatIssuerUrl == null) { + return false; + } + String thatIssuer = thatIssuerUrl.toString(); String thatSessionId = session.getPrincipal().getClaimAsString(LogoutTokenClaimNames.SID); if (thatAudience == null) { return false; @@ -107,10 +113,17 @@ public final class InMemoryOidcSessionRegistry implements OidcSessionRegistry { } private static Predicate subjectMatcher(List audience, String issuer, - String subject) { + @Nullable String subject) { return (session) -> { + if (subject == null) { + return false; + } List thatAudience = session.getPrincipal().getAudience(); - String thatIssuer = session.getPrincipal().getIssuer().toString(); + URL thatIssuerUrl = session.getPrincipal().getIssuer(); + if (thatIssuerUrl == null) { + return false; + } + String thatIssuer = thatIssuerUrl.toString(); String thatSubject = session.getPrincipal().getSubject(); if (thatAudience == null) { return false; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/package-info.java new file mode 100644 index 0000000000..4f2de1f3b6 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/session/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Support for OpenID Connect 1.0 Session Management. + */ +@NullMarked +package org.springframework.security.oauth2.client.oidc.session; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java index 9de5164da0..69f48b9bd8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java @@ -99,6 +99,7 @@ public class OidcUserService implements OAuth2UserService claims = getClaims(userRequest, oauth2User); userInfo = new OidcUserInfo(claims); // https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/package-info.java index 81345d370d..7545c82d41 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/package-info.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/package-info.java @@ -18,4 +18,7 @@ * Classes and interfaces providing support to the client for initiating requests to the * OpenID Connect 1.0 Provider's UserInfo Endpoint. */ +@NullMarked package org.springframework.security.oauth2.client.oidc.userinfo; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java index 5d9972b5e8..d3521ffe2b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java @@ -23,6 +23,7 @@ import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; @@ -49,32 +50,35 @@ public class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSucc private final ClientRegistrationRepository clientRegistrationRepository; - private String postLogoutRedirectUri; + private @Nullable String postLogoutRedirectUri; public OidcClientInitiatedLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) { Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); this.clientRegistrationRepository = clientRegistrationRepository; + this.postLogoutRedirectUri = null; } @Override protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) { + @Nullable Authentication authentication) { String targetUrl = null; if (authentication instanceof OAuth2AuthenticationToken && authentication.getPrincipal() instanceof OidcUser) { String registrationId = ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(); ClientRegistration clientRegistration = this.clientRegistrationRepository .findByRegistrationId(registrationId); - URI endSessionEndpoint = this.endSessionEndpoint(clientRegistration); - if (endSessionEndpoint != null) { - String idToken = idToken(authentication); - String postLogoutRedirectUri = postLogoutRedirectUri(request, clientRegistration); - targetUrl = endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri); + if (clientRegistration != null) { + URI endSessionEndpoint = this.endSessionEndpoint(clientRegistration); + if (endSessionEndpoint != null) { + String idToken = idToken(authentication); + String postLogoutRedirectUri = postLogoutRedirectUri(request, clientRegistration); + targetUrl = endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri); + } } } - return (targetUrl != null) ? targetUrl : super.determineTargetUrl(request, response); + return (targetUrl != null) ? targetUrl : super.determineTargetUrl(request, response, authentication); } - private URI endSessionEndpoint(ClientRegistration clientRegistration) { + private @Nullable URI endSessionEndpoint(@Nullable ClientRegistration clientRegistration) { if (clientRegistration != null) { ProviderDetails providerDetails = clientRegistration.getProviderDetails(); Object endSessionEndpoint = providerDetails.getConfigurationMetadata().get("end_session_endpoint"); @@ -86,11 +90,15 @@ public class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSucc } private String idToken(Authentication authentication) { - return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); + Object principal = authentication.getPrincipal(); + String idToken = (principal instanceof OidcUser oidcUser) ? oidcUser.getIdToken().getTokenValue() : null; + Assert.notNull(idToken, "idToken cannot be null"); + return idToken; } - private String postLogoutRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) { - if (this.postLogoutRedirectUri == null) { + private @Nullable String postLogoutRedirectUri(HttpServletRequest request, + @Nullable ClientRegistration clientRegistration) { + if (this.postLogoutRedirectUri == null || clientRegistration == null) { return null; } // @formatter:off @@ -123,7 +131,7 @@ public class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSucc // @formatter:on } - private String endpointUri(URI endSessionEndpoint, String idToken, String postLogoutRedirectUri) { + private String endpointUri(URI endSessionEndpoint, String idToken, @Nullable String postLogoutRedirectUri) { UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint); builder.queryParam("id_token_hint", idToken); if (postLogoutRedirectUri != null) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/package-info.java new file mode 100644 index 0000000000..bc7e629d4c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Support for OpenID Connect 1.0 Logout (servlet). + */ +@NullMarked +package org.springframework.security.oauth2.client.oidc.web.logout; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java index 6f2922cf8d..66534bad16 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java @@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.convert.converter.Converter; @@ -57,7 +58,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo private final ReactiveClientRegistrationRepository clientRegistrationRepository; - private String postLogoutRedirectUri; + private @Nullable String postLogoutRedirectUri; private Converter> redirectUriResolver = new DefaultRedirectUriResolver(); @@ -94,7 +95,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo // @formatter:on } - private URI endSessionEndpoint(ClientRegistration clientRegistration) { + private @Nullable URI endSessionEndpoint(@Nullable ClientRegistration clientRegistration) { if (clientRegistration != null) { Object endSessionEndpoint = clientRegistration.getProviderDetails() .getConfigurationMetadata() @@ -106,7 +107,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo return null; } - private String endpointUri(URI endSessionEndpoint, String idToken, String postLogoutRedirectUri) { + private String endpointUri(URI endSessionEndpoint, String idToken, @Nullable String postLogoutRedirectUri) { UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint); builder.queryParam("id_token_hint", idToken); if (postLogoutRedirectUri != null) { @@ -116,10 +117,13 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo } private String idToken(Authentication authentication) { - return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); + Object principal = authentication.getPrincipal(); + String idToken = (principal instanceof OidcUser oidcUser) ? oidcUser.getIdToken().getTokenValue() : null; + Assert.notNull(idToken, "idToken cannot be null"); + return idToken; } - private String postLogoutRedirectUri(ServerHttpRequest request, ClientRegistration clientRegistration) { + private @Nullable String postLogoutRedirectUri(ServerHttpRequest request, ClientRegistration clientRegistration) { if (this.postLogoutRedirectUri == null) { return null; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/package-info.java new file mode 100644 index 0000000000..5892902257 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Support for OpenID Connect 1.0 Logout (reactive). + */ +@NullMarked +package org.springframework.security.oauth2.client.oidc.web.server.logout; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/package-info.java index 288b7196b6..e89cac7361 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/package-info.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/package-info.java @@ -17,4 +17,7 @@ /** * Core classes and interfaces providing support for OAuth 2.0 Client. */ +@NullMarked package org.springframework.security.oauth2.client; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index e6d99c1e3f..1222ab9f5a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -32,6 +32,7 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.log.LogMessage; import org.springframework.security.oauth2.core.AuthenticationMethod; @@ -54,25 +55,25 @@ public final class ClientRegistration implements Serializable { private static final long serialVersionUID = 620L; - private String registrationId; + private @Nullable String registrationId; - private String clientId; + private @Nullable String clientId; - private String clientSecret; + private @Nullable String clientSecret; - private ClientAuthenticationMethod clientAuthenticationMethod; + private @Nullable ClientAuthenticationMethod clientAuthenticationMethod; - private AuthorizationGrantType authorizationGrantType; + private @Nullable AuthorizationGrantType authorizationGrantType; - private String redirectUri; + private @Nullable String redirectUri; private Set scopes = Collections.emptySet(); private ProviderDetails providerDetails = new ProviderDetails(); - private String clientName; + private @Nullable String clientName; - private ClientSettings clientSettings; + private @Nullable ClientSettings clientSettings; private ClientRegistration() { } @@ -82,6 +83,7 @@ public final class ClientRegistration implements Serializable { * @return the identifier for the registration */ public String getRegistrationId() { + Assert.notNull(this.registrationId, "registrationId cannot be null"); return this.registrationId; } @@ -90,6 +92,7 @@ public final class ClientRegistration implements Serializable { * @return the client identifier */ public String getClientId() { + Assert.notNull(this.clientId, "clientId cannot be null"); return this.clientId; } @@ -98,6 +101,7 @@ public final class ClientRegistration implements Serializable { * @return the client secret */ public String getClientSecret() { + Assert.notNull(this.clientSecret, "clientSecret cannot be null"); return this.clientSecret; } @@ -107,6 +111,7 @@ public final class ClientRegistration implements Serializable { * @return the {@link ClientAuthenticationMethod} */ public ClientAuthenticationMethod getClientAuthenticationMethod() { + Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null"); return this.clientAuthenticationMethod; } @@ -116,6 +121,7 @@ public final class ClientRegistration implements Serializable { * @return the {@link AuthorizationGrantType} */ public AuthorizationGrantType getAuthorizationGrantType() { + Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null"); return this.authorizationGrantType; } @@ -137,7 +143,7 @@ public final class ClientRegistration implements Serializable { * @return the uri (or uri template) for the redirection endpoint * @since 5.4 */ - public String getRedirectUri() { + public @Nullable String getRedirectUri() { return this.redirectUri; } @@ -162,6 +168,7 @@ public final class ClientRegistration implements Serializable { * @return the client or registration name */ public String getClientName() { + Assert.notNull(this.clientName, "clientName cannot be null"); return this.clientName; } @@ -170,6 +177,7 @@ public final class ClientRegistration implements Serializable { * @return the {@link ClientSettings} */ public ClientSettings getClientSettings() { + Assert.notNull(this.clientSettings, "clientSettings cannot be null"); return this.clientSettings; } @@ -220,15 +228,15 @@ public final class ClientRegistration implements Serializable { private static final long serialVersionUID = 620L; - private String authorizationUri; + private @Nullable String authorizationUri; - private String tokenUri; + private @Nullable String tokenUri; private UserInfoEndpoint userInfoEndpoint = new UserInfoEndpoint(); - private String jwkSetUri; + private @Nullable String jwkSetUri; - private String issuerUri; + private @Nullable String issuerUri; private Map configurationMetadata = Collections.emptyMap(); @@ -237,9 +245,10 @@ public final class ClientRegistration implements Serializable { /** * Returns the uri for the authorization endpoint. - * @return the uri for the authorization endpoint + * @return the uri for the authorization endpoint, or {@code null} if not set + * (e.g. for grant types that do not use the authorization endpoint) */ - public String getAuthorizationUri() { + public @Nullable String getAuthorizationUri() { return this.authorizationUri; } @@ -248,6 +257,7 @@ public final class ClientRegistration implements Serializable { * @return the uri for the token endpoint */ public String getTokenUri() { + Assert.notNull(this.tokenUri, "tokenUri cannot be null"); return this.tokenUri; } @@ -261,9 +271,10 @@ public final class ClientRegistration implements Serializable { /** * Returns the uri for the JSON Web Key (JWK) Set endpoint. - * @return the uri for the JSON Web Key (JWK) Set endpoint + * @return the uri for the JSON Web Key (JWK) Set endpoint, or {@code null} if not + * set */ - public String getJwkSetUri() { + public @Nullable String getJwkSetUri() { return this.jwkSetUri; } @@ -271,10 +282,10 @@ public final class ClientRegistration implements Serializable { * Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the * OAuth 2.0 Authorization Server. * @return the issuer identifier uri for the OpenID Connect 1.0 provider or the - * OAuth 2.0 Authorization Server + * OAuth 2.0 Authorization Server, or {@code null} if not set * @since 5.4 */ - public String getIssuerUri() { + public @Nullable String getIssuerUri() { return this.issuerUri; } @@ -294,20 +305,20 @@ public final class ClientRegistration implements Serializable { private static final long serialVersionUID = 620L; - private String uri; + private @Nullable String uri; private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER; - private String userNameAttributeName; + private @Nullable String userNameAttributeName; UserInfoEndpoint() { } /** * Returns the uri for the user info endpoint. - * @return the uri for the user info endpoint + * @return the uri for the user info endpoint, or {@code null} if not set */ - public String getUri() { + public @Nullable String getUri() { return this.uri; } @@ -324,9 +335,9 @@ public final class ClientRegistration implements Serializable { * Returns the attribute name used to access the user's name from the user * info response. * @return the attribute name used to access the user's name from the user - * info response + * info response, or {@code null} if not set */ - public String getUserNameAttributeName() { + public @Nullable String getUserNameAttributeName() { return this.userNameAttributeName; } @@ -347,37 +358,37 @@ public final class ClientRegistration implements Serializable { AuthorizationGrantType.AUTHORIZATION_CODE, AuthorizationGrantType.CLIENT_CREDENTIALS, AuthorizationGrantType.REFRESH_TOKEN); - private String registrationId; + private @Nullable String registrationId; - private String clientId; + private @Nullable String clientId; - private String clientSecret; + private @Nullable String clientSecret; - private ClientAuthenticationMethod clientAuthenticationMethod; + private @Nullable ClientAuthenticationMethod clientAuthenticationMethod; - private AuthorizationGrantType authorizationGrantType; + private @Nullable AuthorizationGrantType authorizationGrantType; - private String redirectUri; + private @Nullable String redirectUri; - private Set scopes; + private @Nullable Set scopes; - private String authorizationUri; + private @Nullable String authorizationUri; - private String tokenUri; + private @Nullable String tokenUri; - private String userInfoUri; + private @Nullable String userInfoUri; private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER; - private String userNameAttributeName; + private @Nullable String userNameAttributeName; - private String jwkSetUri; + private @Nullable String jwkSetUri; - private String issuerUri; + private @Nullable String issuerUri; private Map configurationMetadata = Collections.emptyMap(); - private String clientName; + private @Nullable String clientName; private ClientSettings clientSettings = ClientSettings.builder().build(); @@ -392,7 +403,7 @@ public final class ClientRegistration implements Serializable { this.clientAuthenticationMethod = clientRegistration.clientAuthenticationMethod; this.authorizationGrantType = clientRegistration.authorizationGrantType; this.redirectUri = clientRegistration.redirectUri; - this.scopes = (clientRegistration.scopes != null) ? new HashSet<>(clientRegistration.scopes) : null; + this.scopes = new HashSet<>(clientRegistration.getScopes()); this.authorizationUri = clientRegistration.providerDetails.authorizationUri; this.tokenUri = clientRegistration.providerDetails.tokenUri; this.userInfoUri = clientRegistration.providerDetails.userInfoEndpoint.uri; @@ -400,12 +411,11 @@ public final class ClientRegistration implements Serializable { this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName; this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri; this.issuerUri = clientRegistration.providerDetails.issuerUri; - Map configurationMetadata = clientRegistration.providerDetails.configurationMetadata; - if (configurationMetadata != Collections.EMPTY_MAP) { - this.configurationMetadata = new HashMap<>(configurationMetadata); - } + this.configurationMetadata = new HashMap<>(clientRegistration.providerDetails.configurationMetadata); this.clientName = clientRegistration.clientName; - this.clientSettings = clientRegistration.clientSettings; + if (clientRegistration.clientSettings != null) { + this.clientSettings = clientRegistration.clientSettings; + } } /** @@ -433,7 +443,7 @@ public final class ClientRegistration implements Serializable { * @param clientSecret the client secret * @return the {@link Builder} */ - public Builder clientSecret(String clientSecret) { + public Builder clientSecret(@Nullable String clientSecret) { this.clientSecret = clientSecret; return this; } @@ -479,7 +489,7 @@ public final class ClientRegistration implements Serializable { * @return the {@link Builder} * @since 5.4 */ - public Builder redirectUri(String redirectUri) { + public Builder redirectUri(@Nullable String redirectUri) { this.redirectUri = redirectUri; return this; } @@ -489,19 +499,21 @@ public final class ClientRegistration implements Serializable { * @param scope the scope(s) used for the client * @return the {@link Builder} */ - public Builder scope(String... scope) { + // @formatter:off + public Builder scope(String @Nullable... scope) { if (scope != null && scope.length > 0) { this.scopes = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(scope))); } return this; } + // @formatter:on /** * Sets the scope(s) used for the client. * @param scope the scope(s) used for the client * @return the {@link Builder} */ - public Builder scope(Collection scope) { + public Builder scope(@Nullable Collection scope) { if (scope != null && !scope.isEmpty()) { this.scopes = Collections.unmodifiableSet(new LinkedHashSet<>(scope)); } @@ -513,7 +525,7 @@ public final class ClientRegistration implements Serializable { * @param authorizationUri the uri for the authorization endpoint * @return the {@link Builder} */ - public Builder authorizationUri(String authorizationUri) { + public Builder authorizationUri(@Nullable String authorizationUri) { this.authorizationUri = authorizationUri; return this; } @@ -533,7 +545,7 @@ public final class ClientRegistration implements Serializable { * @param userInfoUri the uri for the user info endpoint * @return the {@link Builder} */ - public Builder userInfoUri(String userInfoUri) { + public Builder userInfoUri(@Nullable String userInfoUri) { this.userInfoUri = userInfoUri; return this; } @@ -557,7 +569,7 @@ public final class ClientRegistration implements Serializable { * from the user info response * @return the {@link Builder} */ - public Builder userNameAttributeName(String userNameAttributeName) { + public Builder userNameAttributeName(@Nullable String userNameAttributeName) { this.userNameAttributeName = userNameAttributeName; return this; } @@ -567,7 +579,7 @@ public final class ClientRegistration implements Serializable { * @param jwkSetUri the uri for the JSON Web Key (JWK) Set endpoint * @return the {@link Builder} */ - public Builder jwkSetUri(String jwkSetUri) { + public Builder jwkSetUri(@Nullable String jwkSetUri) { this.jwkSetUri = jwkSetUri; return this; } @@ -580,7 +592,7 @@ public final class ClientRegistration implements Serializable { * @return the {@link Builder} * @since 5.4 */ - public Builder issuerUri(String issuerUri) { + public Builder issuerUri(@Nullable String issuerUri) { this.issuerUri = issuerUri; return this; } @@ -592,7 +604,7 @@ public final class ClientRegistration implements Serializable { * @return the {@link Builder} * @since 5.1 */ - public Builder providerConfigurationMetadata(Map configurationMetadata) { + public Builder providerConfigurationMetadata(@Nullable Map configurationMetadata) { if (configurationMetadata != null) { this.configurationMetadata = new LinkedHashMap<>(configurationMetadata); } @@ -604,7 +616,7 @@ public final class ClientRegistration implements Serializable { * @param clientName the client or registration name * @return the {@link Builder} */ - public Builder clientName(String clientName) { + public Builder clientName(@Nullable String clientName) { this.clientName = clientName; return this; } @@ -615,7 +627,6 @@ public final class ClientRegistration implements Serializable { * @return the {@link Builder} */ public Builder clientSettings(ClientSettings clientSettings) { - Assert.notNull(clientSettings, "clientSettings cannot be null"); this.clientSettings = clientSettings; return this; } @@ -643,10 +654,10 @@ public final class ClientRegistration implements Serializable { clientRegistration.clientId = this.clientId; clientRegistration.clientSecret = StringUtils.hasText(this.clientSecret) ? this.clientSecret : ""; clientRegistration.clientAuthenticationMethod = (this.clientAuthenticationMethod != null) - ? this.clientAuthenticationMethod : deduceClientAuthenticationMethod(clientRegistration); + ? this.clientAuthenticationMethod : deduceClientAuthenticationMethod(); clientRegistration.authorizationGrantType = this.authorizationGrantType; clientRegistration.redirectUri = this.redirectUri; - clientRegistration.scopes = this.scopes; + clientRegistration.scopes = (this.scopes != null) ? this.scopes : Collections.emptySet(); clientRegistration.providerDetails = createProviderDetails(clientRegistration); clientRegistration.clientName = StringUtils.hasText(this.clientName) ? this.clientName : this.registrationId; @@ -654,7 +665,8 @@ public final class ClientRegistration implements Serializable { return clientRegistration; } - private ClientAuthenticationMethod deduceClientAuthenticationMethod(ClientRegistration clientRegistration) { + private ClientAuthenticationMethod deduceClientAuthenticationMethod() { + Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null"); if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType) && (!StringUtils.hasText(this.clientSecret))) { return ClientAuthenticationMethod.NONE; @@ -676,6 +688,7 @@ public final class ClientRegistration implements Serializable { } private void validateAuthorizationCodeGrantType() { + Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null"); Assert.isTrue(AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizationGrantType), () -> "authorizationGrantType must be " + AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); Assert.hasText(this.registrationId, "registrationId cannot be empty"); @@ -686,6 +699,7 @@ public final class ClientRegistration implements Serializable { } private void validateClientCredentialsGrantType() { + Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null"); Assert.isTrue(AuthorizationGrantType.CLIENT_CREDENTIALS.equals(this.authorizationGrantType), () -> "authorizationGrantType must be " + AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); Assert.hasText(this.registrationId, "registrationId cannot be empty"); @@ -694,6 +708,7 @@ public final class ClientRegistration implements Serializable { } private void validateAuthorizationGrantTypes() { + Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null"); for (AuthorizationGrantType authorizationGrantType : AUTHORIZATION_GRANT_TYPES) { if (authorizationGrantType.getValue().equalsIgnoreCase(this.authorizationGrantType.getValue()) && !authorizationGrantType.equals(this.authorizationGrantType)) { @@ -718,7 +733,7 @@ public final class ClientRegistration implements Serializable { } private static boolean validateScope(String scope) { - return scope == null || scope.chars() + return scope.chars() .allMatch((c) -> withinTheRangeOf(c, 0x21, 0x21) || withinTheRangeOf(c, 0x23, 0x5B) || withinTheRangeOf(c, 0x5D, 0x7E)); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java index 0b7cf44ff9..ad0c519b94 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrationRepository.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.client.registration; +import org.jspecify.annotations.Nullable; + /** * A repository for OAuth 2.0 / OpenID Connect 1.0 {@link ClientRegistration}(s). * @@ -37,6 +39,6 @@ public interface ClientRegistrationRepository { * @param registrationId the registration identifier * @return the {@link ClientRegistration} if found, otherwise {@code null} */ - ClientRegistration findByRegistrationId(String registrationId); + @Nullable ClientRegistration findByRegistrationId(String registrationId); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java index 5e45b1858a..6d9a3cc35a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java @@ -27,6 +27,7 @@ import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata; import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; import net.minidev.json.JSONObject; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.RequestEntity; @@ -199,6 +200,7 @@ public final class ClientRegistrations { return () -> { RequestEntity request = RequestEntity.get(uri.toUriString()).build(); Map configuration = rest.exchange(request, typeReference).getBody(); + Assert.notNull(configuration, "OIDC provider configuration cannot be null"); OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse); ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer) .jwkSetUri(metadata.getJWKSetURI().toASCIIString()); @@ -249,6 +251,7 @@ public final class ClientRegistrations { return () -> { RequestEntity request = RequestEntity.get(uri.toUriString()).build(); Map configuration = rest.exchange(request, typeReference).getBody(); + Assert.notNull(configuration, "Authorization server configuration cannot be null"); AuthorizationServerMetadata metadata = parse(configuration, AuthorizationServerMetadata::parse); ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer); URI jwkSetUri = metadata.getJWKSetURI(); @@ -318,22 +321,29 @@ public final class ClientRegistrations { + "not match the requested issuer \"" + issuer + "\""); String name = URI.create(issuer).getHost(); ClientAuthenticationMethod method = getClientAuthenticationMethod(metadata.getTokenEndpointAuthMethods()); + URI authorizationEndpointURI = metadata.getAuthorizationEndpointURI(); + URI tokenEndpointURI = metadata.getTokenEndpointURI(); + ClientAuthenticationMethod authMethod = (method != null) ? method + : ClientAuthenticationMethod.CLIENT_SECRET_BASIC; Map configurationMetadata = new LinkedHashMap<>(metadata.toJSONObject()); // @formatter:off - return ClientRegistration.withRegistrationId(name) + ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(name) .userNameAttributeName(IdTokenClaimNames.SUB) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .clientAuthenticationMethod(method) + .clientAuthenticationMethod(authMethod) .redirectUri("{baseUrl}/{action}/oauth2/code/{registrationId}") - .authorizationUri((metadata.getAuthorizationEndpointURI() != null) ? metadata.getAuthorizationEndpointURI().toASCIIString() : null) + .authorizationUri((authorizationEndpointURI != null) ? authorizationEndpointURI.toASCIIString() : null) .providerConfigurationMetadata(configurationMetadata) - .tokenUri(metadata.getTokenEndpointURI().toASCIIString()) .issuerUri(issuer) .clientName(issuer); + if (tokenEndpointURI != null) { + builder.tokenUri(tokenEndpointURI.toASCIIString()); + } + return builder; // @formatter:on } - private static ClientAuthenticationMethod getClientAuthenticationMethod( + private static @Nullable ClientAuthenticationMethod getClientAuthenticationMethod( List metadataAuthMethods) { if (metadataAuthMethods == null || metadataAuthMethods .contains(com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java index e07419dea9..437e6c0be0 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/InMemoryClientRegistrationRepository.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -87,7 +89,7 @@ public final class InMemoryClientRegistrationRepository } @Override - public ClientRegistration findByRegistrationId(String registrationId) { + public @Nullable ClientRegistration findByRegistrationId(String registrationId) { Assert.hasText(registrationId, "registrationId cannot be empty"); return this.registrations.get(registrationId); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepository.java index 402e9d5039..1cc5ff1ba6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/SupplierClientRegistrationRepository.java @@ -19,6 +19,8 @@ package org.springframework.security.oauth2.client.registration; import java.util.Iterator; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.function.SingletonSupplier; @@ -48,7 +50,7 @@ public final class SupplierClientRegistrationRepository } @Override - public ClientRegistration findByRegistrationId(String registrationId) { + public @Nullable ClientRegistration findByRegistrationId(String registrationId) { Assert.hasText(registrationId, "registrationId cannot be empty"); return this.repositorySupplier.get().findByRegistrationId(registrationId); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/package-info.java index 24d1a563e9..1220280e4f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/package-info.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/package-info.java @@ -18,4 +18,7 @@ * Classes and interfaces that provide support for * {@link org.springframework.security.oauth2.client.registration.ClientRegistration}. */ +@NullMarked package org.springframework.security.oauth2.client.registration; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java index 76248322e0..1a4b6bed9f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java @@ -94,7 +94,9 @@ public class DefaultOAuth2UserService implements OAuth2UserService request = this.requestEntityConverter.convert(userRequest); ResponseEntity> response = getResponse(userRequest, request); OAuth2AccessToken token = userRequest.getAccessToken(); - Map attributes = this.attributesConverter.convert(userRequest).convert(response.getBody()); + Map body = response.getBody(); + Assert.notNull(body, "userInfo response body cannot be null"); + Map attributes = this.attributesConverter.convert(userRequest).convert(body); Collection authorities = getAuthorities(token, attributes, userNameAttributeName); return new DefaultOAuth2User(authorities, attributes, userNameAttributeName); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java index 3a69c93122..452ba95a0d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java @@ -140,11 +140,12 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi return new DefaultOAuth2User(authorities, attrs, userNameAttributeName); }) - .onErrorMap((ex) -> (ex instanceof UnsupportedMediaTypeException || - ex.getCause() instanceof UnsupportedMediaTypeException), (ex) -> { - String contentType = (ex instanceof UnsupportedMediaTypeException) ? - ((UnsupportedMediaTypeException) ex).getContentType().toString() : - ((UnsupportedMediaTypeException) ex.getCause()).getContentType().toString(); + .onErrorMap((ex) -> (ex instanceof UnsupportedMediaTypeException + || (ex.getCause() != null && ex.getCause() instanceof UnsupportedMediaTypeException)), (ex) -> { + UnsupportedMediaTypeException umte = (ex instanceof UnsupportedMediaTypeException) + ? (UnsupportedMediaTypeException) ex : (UnsupportedMediaTypeException) ex.getCause(); + String contentType = (umte != null && umte.getContentType() != null) + ? umte.getContentType().toString() : "unknown"; String errorMessage = "An error occurred while attempting to retrieve the UserInfo Resource from '" + userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint() .getUri() diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DelegatingOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DelegatingOAuth2UserService.java index e9c03e5423..9d8e4e600f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DelegatingOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DelegatingOAuth2UserService.java @@ -21,6 +21,8 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; @@ -56,7 +58,7 @@ public class DelegatingOAuth2UserService convert(OAuth2UserRequest userRequest) { ClientRegistration clientRegistration = userRequest.getClientRegistration(); + String userInfoUri = clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri(); + Assert.hasText(userInfoUri, "UserInfo Endpoint Uri is required"); HttpMethod httpMethod = getHttpMethod(clientRegistration); HttpHeaders headers = new HttpHeaders(); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - URI uri = UriComponentsBuilder - .fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()) - .build() - .toUri(); + URI uri = UriComponentsBuilder.fromUriString(userInfoUri).build().toUri(); RequestEntity request; if (HttpMethod.POST.equals(httpMethod)) { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserService.java index 40d95a7393..b44802029f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/OAuth2UserService.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.client.userinfo; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -46,6 +48,6 @@ public interface OAuth2UserService T loadAuthorizedClient(String clientRegistrationId, + public @Nullable T loadAuthorizedClient(String clientRegistrationId, Authentication principal, HttpServletRequest request) { if (this.isPrincipalAuthenticated(principal)) { return this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java index 125976a80e..e2e0159de1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthorizationRequestRepository.java @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -45,7 +46,7 @@ public interface AuthorizationRequestRepository attributes) { + public static @Nullable String resolveClientRegistrationId(Map attributes) { return (String) attributes.get(CLIENT_REGISTRATION_ID_ATTR_NAME); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java index a420171ae3..2fa93b0e22 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java @@ -22,9 +22,11 @@ import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; import org.springframework.security.crypto.keygen.StringKeyGenerator; @@ -118,7 +120,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au } @Override - public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { + public @Nullable OAuth2AuthorizationRequest resolve(HttpServletRequest request) { String registrationId = resolveRegistrationId(request); if (registrationId == null) { return null; @@ -128,7 +130,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au } @Override - public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) { + public @Nullable OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) { if (registrationId == null) { return null; } @@ -158,7 +160,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au return action; } - private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, + private @Nullable OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) { if (registrationId == null) { return null; @@ -171,9 +173,11 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction); + String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri(); + Assert.hasText(authorizationUri, "Authorization URI is required"); // @formatter:off builder.clientId(clientRegistration.getClientId()) - .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .authorizationUri(authorizationUri) .redirectUri(redirectUriStr) .scopes(clientRegistration.getScopes()) .state(DEFAULT_STATE_GENERATOR.generateKey()); @@ -210,7 +214,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au + ") for Client Registration with Id: " + clientRegistration.getRegistrationId()); } - private String resolveRegistrationId(HttpServletRequest request) { + private @Nullable String resolveRegistrationId(HttpServletRequest request) { if (this.authorizationRequestMatcher.matches(request)) { return this.authorizationRequestMatcher.matcher(request) .getVariables() @@ -263,7 +267,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au uriVariables.put("basePath", (path != null) ? path : ""); uriVariables.put("baseUrl", uriComponents.toUriString()); uriVariables.put("action", (action != null) ? action : ""); - return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUri()) + return UriComponentsBuilder.fromUriString(Objects.requireNonNull(clientRegistration.getRedirectUri())) .buildAndExpand(uriVariables) .toUriString(); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java index 935ede35c4..b41a8cd5ce 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java @@ -23,8 +23,8 @@ import java.util.function.Function; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; @@ -121,15 +121,24 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori this.authorizedClientRepository = authorizedClientRepository; this.authorizedClientProvider = DEFAULT_AUTHORIZED_CLIENT_PROVIDER; this.contextAttributesMapper = new DefaultContextAttributesMapper(); - this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> authorizedClientRepository - .saveAuthorizedClient(authorizedClient, principal, - (HttpServletRequest) attributes.get(HttpServletRequest.class.getName()), - (HttpServletResponse) attributes.get(HttpServletResponse.class.getName())); + this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> { + HttpServletRequest request = (HttpServletRequest) attributes.get(HttpServletRequest.class.getName()); + HttpServletResponse response = (HttpServletResponse) attributes.get(HttpServletResponse.class.getName()); + Assert.notNull(request, "HttpServletRequest is required"); + Assert.notNull(response, "HttpServletResponse is required"); + authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, request, response); + }; this.authorizationFailureHandler = new RemoveAuthorizedClientOAuth2AuthorizationFailureHandler( - (clientRegistrationId, principal, attributes) -> authorizedClientRepository.removeAuthorizedClient( - clientRegistrationId, principal, - (HttpServletRequest) attributes.get(HttpServletRequest.class.getName()), - (HttpServletResponse) attributes.get(HttpServletResponse.class.getName()))); + (clientRegistrationId, principal, attributes) -> { + HttpServletRequest request = (HttpServletRequest) attributes + .get(HttpServletRequest.class.getName()); + HttpServletResponse response = (HttpServletResponse) attributes + .get(HttpServletResponse.class.getName()); + Assert.notNull(request, "HttpServletRequest is required"); + Assert.notNull(response, "HttpServletResponse is required"); + authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, request, + response); + }); } @Nullable @@ -203,7 +212,7 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori return attributes; } - private static HttpServletRequest getHttpServletRequestOrDefault(Map attributes) { + private static @Nullable HttpServletRequest getHttpServletRequestOrDefault(Map attributes) { HttpServletRequest servletRequest = (HttpServletRequest) attributes.get(HttpServletRequest.class.getName()); if (servletRequest == null) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); @@ -214,7 +223,7 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori return servletRequest; } - private static HttpServletResponse getHttpServletResponseOrDefault(Map attributes) { + private static @Nullable HttpServletResponse getHttpServletResponseOrDefault(Map attributes) { HttpServletResponse servletResponse = (HttpServletResponse) attributes.get(HttpServletResponse.class.getName()); if (servletResponse == null) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); @@ -294,11 +303,13 @@ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2Authori public Map apply(OAuth2AuthorizeRequest authorizeRequest) { Map contextAttributes = Collections.emptyMap(); HttpServletRequest servletRequest = getHttpServletRequestOrDefault(authorizeRequest.getAttributes()); - String scope = servletRequest.getParameter(OAuth2ParameterNames.SCOPE); - if (StringUtils.hasText(scope)) { - contextAttributes = new HashMap<>(); - contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME, - StringUtils.delimitedListToStringArray(scope, " ")); + if (servletRequest != null) { + String scope = servletRequest.getParameter(OAuth2ParameterNames.SCOPE); + if (StringUtils.hasText(scope)) { + contextAttributes = new HashMap<>(); + contextAttributes.put(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME, + StringUtils.delimitedListToStringArray(scope, " ")); + } } return contextAttributes; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultReactiveOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultReactiveOAuth2AuthorizedClientManager.java index e76e765d8a..6c156ad558 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultReactiveOAuth2AuthorizedClientManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultReactiveOAuth2AuthorizedClientManager.java @@ -132,13 +132,22 @@ public final class DefaultReactiveOAuth2AuthorizedClientManager implements React Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); this.clientRegistrationRepository = clientRegistrationRepository; this.authorizedClientRepository = authorizedClientRepository; - this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> authorizedClientRepository - .saveAuthorizedClient(authorizedClient, principal, - (ServerWebExchange) attributes.get(ServerWebExchange.class.getName())); + this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> { + ServerWebExchange exchange = (ServerWebExchange) attributes.get(ServerWebExchange.class.getName()); + if (exchange != null) { + return authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, exchange); + } + return Mono.empty(); + }; this.authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler( - (clientRegistrationId, principal, attributes) -> authorizedClientRepository.removeAuthorizedClient( - clientRegistrationId, principal, - (ServerWebExchange) attributes.get(ServerWebExchange.class.getName()))); + (clientRegistrationId, principal, attributes) -> { + ServerWebExchange exchange = (ServerWebExchange) attributes.get(ServerWebExchange.class.getName()); + if (exchange != null) { + return authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, + exchange); + } + return Mono.empty(); + }); } @Override diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java index b5b4aa712e..4a95ce1d9a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizationRequestRepository.java @@ -19,6 +19,7 @@ package org.springframework.security.oauth2.client.web; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import org.jspecify.annotations.Nullable; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -44,7 +45,7 @@ public final class HttpSessionOAuth2AuthorizationRequestRepository private final String sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME; @Override - public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + public @Nullable OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { Assert.notNull(request, "request cannot be null"); String stateParameter = getStateParameter(request); if (stateParameter == null) { @@ -70,7 +71,7 @@ public final class HttpSessionOAuth2AuthorizationRequestRepository } @Override - public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, + public @Nullable OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) { Assert.notNull(response, "response cannot be null"); OAuth2AuthorizationRequest authorizationRequest = loadAuthorizationRequest(request); @@ -85,11 +86,11 @@ public final class HttpSessionOAuth2AuthorizationRequestRepository * @param request the request to use * @return the state parameter or null if not found */ - private String getStateParameter(HttpServletRequest request) { + private @Nullable String getStateParameter(HttpServletRequest request) { return request.getParameter(OAuth2ParameterNames.STATE); } - private OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request) { + private @Nullable OAuth2AuthorizationRequest getAuthorizationRequest(HttpServletRequest request) { HttpSession session = request.getSession(false); return (session != null) ? (OAuth2AuthorizationRequest) session.getAttribute(this.sessionAttributeName) : null; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java index eabb77ae97..9f802a1aee 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/HttpSessionOAuth2AuthorizedClientRepository.java @@ -22,6 +22,7 @@ import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import org.jspecify.annotations.Nullable; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; @@ -45,7 +46,7 @@ public final class HttpSessionOAuth2AuthorizedClientRepository implements OAuth2 @SuppressWarnings("unchecked") @Override - public T loadAuthorizedClient(String clientRegistrationId, + public @Nullable T loadAuthorizedClient(String clientRegistrationId, Authentication principal, HttpServletRequest request) { Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty"); Assert.notNull(request, "request cannot be null"); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java index ec6b9ae2a5..1e838855cf 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java @@ -28,9 +28,11 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; @@ -193,7 +195,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { if (authorizationRequest == null) { return false; } - // Compare redirect_uri + Assert.notNull(authorizationRequest.getRedirectUri(), "redirectUri cannot be null"); UriComponents requestUri = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request)).build(); UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()).build(); Set>> requestUriParameters = new LinkedHashSet<>( @@ -220,8 +222,11 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { throws IOException { OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository .removeAuthorizationRequest(request, response); + Assert.notNull(authorizationRequest, "authorizationRequest cannot be null"); String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID); + Assert.hasText(registrationId, "registrationId cannot be empty"); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); + Assert.notNull(clientRegistration, "Client registration not found with id: " + registrationId); MultiValueMap params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); String redirectUri = UrlUtils.buildFullRequestUrl(request); OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, @@ -236,7 +241,9 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { } catch (OAuth2AuthorizationException ex) { OAuth2Error error = ex.getError(); - UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authorizationRequest.getRedirectUri()) + String errorRedirectUri = authorizationRequest.getRedirectUri(); + Assert.hasText(errorRedirectUri, "redirectUri cannot be empty"); + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(errorRedirectUri) .queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode()); if (StringUtils.hasLength(error.getDescription())) { uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription()); @@ -249,17 +256,21 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { } Authentication currentAuthentication = this.securityContextHolderStrategy.getContext().getAuthentication(); String principalName = (currentAuthentication != null) ? currentAuthentication.getName() : "anonymousUser"; + Authentication principal = (currentAuthentication != null) ? currentAuthentication + : new AnonymousAuthenticationToken("anonymous", principalName, + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + Assert.notNull(authenticationResult.getAccessToken(), "accessToken cannot be null"); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), principalName, authenticationResult.getAccessToken(), authenticationResult.getRefreshToken()); - this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, currentAuthentication, request, - response); + this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, request, response); String redirectUrl = authorizationRequest.getRedirectUri(); SavedRequest savedRequest = this.requestCache.getRequest(request, response); if (savedRequest != null) { redirectUrl = savedRequest.getRedirectUrl(); this.requestCache.removeRequest(request, response); } + Assert.hasText(redirectUrl, "redirectUrl cannot be empty"); this.redirectStrategy.sendRedirect(request, response, redirectUrl); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java index a792a306a8..8baca2a962 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java @@ -240,14 +240,16 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt private void unsuccessfulRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws IOException { Throwable cause = ex.getCause(); - LogMessage message = LogMessage.format("Authorization Request failed: %s", cause); - if (InvalidClientRegistrationIdException.class.isAssignableFrom(cause.getClass())) { - // Log an invalid registrationId at WARN level to allow these errors to be - // tuned separately from other errors - this.logger.warn(message, ex); - } - else { - this.logger.error(message, ex); + if (cause != null) { + LogMessage message = LogMessage.format("Authorization Request failed: %s", cause); + if (InvalidClientRegistrationIdException.class.isAssignableFrom(cause.getClass())) { + // Log an invalid registrationId at WARN level to allow these errors to be + // tuned separately from other errors + this.logger.warn(message, ex); + } + else { + this.logger.error(message, ex); + } } response.sendError(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java index 4924a1c963..89812810bb 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestResolver.java @@ -17,6 +17,7 @@ package org.springframework.security.oauth2.client.web; import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; @@ -41,7 +42,7 @@ public interface OAuth2AuthorizationRequestResolver { * @return the resolved {@link OAuth2AuthorizationRequest} or {@code null} if not * available */ - OAuth2AuthorizationRequest resolve(HttpServletRequest request); + @Nullable OAuth2AuthorizationRequest resolve(HttpServletRequest request); /** * Returns the {@link OAuth2AuthorizationRequest} resolved from the provided @@ -51,6 +52,6 @@ public interface OAuth2AuthorizationRequestResolver { * @return the resolved {@link OAuth2AuthorizationRequest} or {@code null} if not * available */ - OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId); + @Nullable OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java index 6b1499165e..b24817e414 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java @@ -67,18 +67,30 @@ final class OAuth2AuthorizationResponseUtils { String errorCode = request.getFirst(OAuth2ParameterNames.ERROR); String state = request.getFirst(OAuth2ParameterNames.STATE); if (StringUtils.hasText(code)) { - return OAuth2AuthorizationResponse.success(code).redirectUri(redirectUri).state(state).build(); + OAuth2AuthorizationResponse.Builder builder = OAuth2AuthorizationResponse.success(code) + .redirectUri(redirectUri); + if (state != null) { + builder.state(state); + } + return builder.build(); + } + if (!StringUtils.hasText(errorCode)) { + errorCode = "unknown_error"; } String errorDescription = request.getFirst(OAuth2ParameterNames.ERROR_DESCRIPTION); String errorUri = request.getFirst(OAuth2ParameterNames.ERROR_URI); - // @formatter:off - return OAuth2AuthorizationResponse.error(errorCode) - .redirectUri(redirectUri) - .errorDescription(errorDescription) - .errorUri(errorUri) - .state(state) - .build(); - // @formatter:on + OAuth2AuthorizationResponse.Builder builder = OAuth2AuthorizationResponse.error(errorCode) + .redirectUri(redirectUri); + if (errorDescription != null) { + builder.errorDescription(errorDescription); + } + if (errorUri != null) { + builder.errorUri(errorUri); + } + if (state != null) { + builder.state(state); + } + return builder.build(); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java index c9e99a2159..ad396077b1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizedClientRepository.java @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; @@ -54,8 +55,8 @@ public interface OAuth2AuthorizedClientRepository { * @param a type of OAuth2AuthorizedClient * @return the {@link OAuth2AuthorizedClient} or {@code null} if not available */ - T loadAuthorizedClient(String clientRegistrationId, Authentication principal, - HttpServletRequest request); + @Nullable T loadAuthorizedClient(String clientRegistrationId, + Authentication principal, HttpServletRequest request); /** * Saves the {@link OAuth2AuthorizedClient} associating it to the provided End-User diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java index 551e49cad5..314e866a83 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -178,6 +178,7 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID); + Assert.hasText(registrationId, "registrationId cannot be empty"); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); if (clientRegistration == null) { OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE, @@ -203,6 +204,7 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce .convert(authenticationResult); Assert.notNull(oauth2Authentication, "authentication result cannot be null"); oauth2Authentication.setDetails(authenticationDetails); + Assert.notNull(authenticationResult.getAccessToken(), "accessToken cannot be null"); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), oauth2Authentication.getName(), authenticationResult.getAccessToken(), authenticationResult.getRefreshToken()); @@ -237,6 +239,7 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce } private OAuth2AuthenticationToken createAuthenticationResult(OAuth2LoginAuthenticationToken authenticationResult) { + Assert.notNull(authenticationResult.getPrincipal(), "principal cannot be null"); return new OAuth2AuthenticationToken(authenticationResult.getPrincipal(), authenticationResult.getAuthorities(), authenticationResult.getClientRegistration().getRegistrationId()); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptor.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptor.java index fcf138d4c2..f906491b51 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptor.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptor.java @@ -22,6 +22,7 @@ import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; @@ -30,7 +31,6 @@ import org.springframework.http.HttpStatusCode; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.Nullable; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; @@ -186,8 +186,10 @@ public final class OAuth2ClientHttpRequestInterceptor implements ClientHttpReque .get(HttpServletRequest.class.getName()); HttpServletResponse response = (HttpServletResponse) attributes .get(HttpServletResponse.class.getName()); - authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, request, - response); + if (request != null && response != null) { + authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, request, + response); + } }); } @@ -256,7 +258,9 @@ public final class OAuth2ClientHttpRequestInterceptor implements ClientHttpReque return response; } catch (RestClientResponseException ex) { - handleAuthorizationFailure(request, principal, ex.getResponseHeaders(), ex.getStatusCode()); + HttpHeaders responseHeaders = ex.getResponseHeaders(); + handleAuthorizationFailure(request, principal, + (responseHeaders != null) ? responseHeaders : new HttpHeaders(), ex.getStatusCode()); throw ex; } catch (OAuth2AuthorizationException ex) { @@ -297,7 +301,7 @@ public final class OAuth2ClientHttpRequestInterceptor implements ClientHttpReque handleAuthorizationFailure(authorizationException, principal); } - private static OAuth2Error resolveOAuth2ErrorIfPossible(HttpHeaders headers, HttpStatusCode httpStatus) { + private static @Nullable OAuth2Error resolveOAuth2ErrorIfPossible(HttpHeaders headers, HttpStatusCode httpStatus) { String wwwAuthenticateHeader = headers.getFirst(HttpHeaders.WWW_AUTHENTICATE); if (wwwAuthenticateHeader != null) { Map parameters = parseWwwAuthenticateHeader(wwwAuthenticateHeader); @@ -366,8 +370,7 @@ public final class OAuth2ClientHttpRequestInterceptor implements ClientHttpReque * @return the {@code clientRegistrationId} to be used for resolving an * {@link OAuth2AuthorizedClient}. */ - @Nullable - String resolve(HttpRequest request); + @Nullable String resolve(HttpRequest request); } @@ -386,8 +389,7 @@ public final class OAuth2ClientHttpRequestInterceptor implements ClientHttpReque * @return the {@link Authentication principal} to be used for resolving an * {@link OAuth2AuthorizedClient}. */ - @Nullable - Authentication resolve(HttpRequest request); + @Nullable Authentication resolve(HttpRequest request); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributeClientRegistrationIdResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributeClientRegistrationIdResolver.java index 4904887bf9..dc50e9ec34 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributeClientRegistrationIdResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributeClientRegistrationIdResolver.java @@ -19,6 +19,8 @@ package org.springframework.security.oauth2.client.web.client; import java.util.Map; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; @@ -37,7 +39,7 @@ public final class RequestAttributeClientRegistrationIdResolver implements OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver { @Override - public String resolve(HttpRequest request) { + public @Nullable String resolve(HttpRequest request) { return ClientAttributes.resolveClientRegistrationId(request.getAttributes()); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributePrincipalResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributePrincipalResolver.java index 5dda171f13..b96ff59a0f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributePrincipalResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributePrincipalResolver.java @@ -20,6 +20,8 @@ import java.util.Collections; import java.util.Map; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequest; import org.springframework.security.authentication.AbstractAuthenticationToken; @@ -40,7 +42,7 @@ public class RequestAttributePrincipalResolver implements OAuth2ClientHttpReques .concat(".principal"); @Override - public Authentication resolve(HttpRequest request) { + public @Nullable Authentication resolve(HttpRequest request) { return (Authentication) request.getAttributes().get(PRINCIPAL_ATTR_NAME); } @@ -79,7 +81,7 @@ public class RequestAttributePrincipalResolver implements OAuth2ClientHttpReques } @Override - public Object getCredentials() { + public @Nullable Object getCredentials() { return null; } }; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/SecurityContextHolderPrincipalResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/SecurityContextHolderPrincipalResolver.java index 42c9c4b2b5..83411ad9ed 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/SecurityContextHolderPrincipalResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/SecurityContextHolderPrincipalResolver.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.client.web.client; +import org.jspecify.annotations.Nullable; + import org.springframework.http.HttpRequest; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -50,7 +52,7 @@ public class SecurityContextHolderPrincipalResolver implements OAuth2ClientHttpR } @Override - public Authentication resolve(HttpRequest request) { + public @Nullable Authentication resolve(HttpRequest request) { return this.securityContextHolderStrategy.getContext().getAuthentication(); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/package-info.java new file mode 100644 index 0000000000..61fcf31d68 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Support classes for OAuth2 Client with RestClient and WebClient. + */ +@NullMarked +package org.springframework.security.oauth2.client.web.client; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/support/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/support/package-info.java new file mode 100644 index 0000000000..393301cd3a --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/support/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Support classes for OAuth2 Client with RestClient. + */ +@NullMarked +package org.springframework.security.oauth2.client.web.client.support; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index 1194858879..f9adfef7c0 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -18,11 +18,10 @@ package org.springframework.security.oauth2.client.web.method.annotation; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; @@ -106,9 +105,8 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth .findMergedAnnotation(parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class) != null)); } - @NonNull @Override - public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + public @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { String clientRegistrationId = this.resolveClientRegistrationId(parameter); if (!StringUtils.hasLength(clientRegistrationId)) { @@ -122,6 +120,8 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth } HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); HttpServletResponse servletResponse = webRequest.getNativeResponse(HttpServletResponse.class); + Assert.notNull(servletRequest, "HttpServletRequest is required for OAuth2 authorized client resolution"); + Assert.notNull(servletResponse, "HttpServletResponse is required for OAuth2 authorized client resolution"); // @formatter:off OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest .withClientRegistrationId(clientRegistrationId) @@ -133,9 +133,12 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth return this.authorizedClientManager.authorize(authorizeRequest); } - private String resolveClientRegistrationId(MethodParameter parameter) { + private @Nullable String resolveClientRegistrationId(MethodParameter parameter) { RegisteredOAuth2AuthorizedClient authorizedClientAnnotation = AnnotatedElementUtils .findMergedAnnotation(parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class); + if (authorizedClientAnnotation == null) { + return null; + } Authentication principal = this.securityContextHolderStrategy.getContext().getAuthentication(); if (StringUtils.hasLength(authorizedClientAnnotation.registrationId())) { return authorizedClientAnnotation.registrationId(); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/package-info.java new file mode 100644 index 0000000000..3932d15fe2 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Method annotation support for OAuth2 client. + */ +@NullMarked +package org.springframework.security.oauth2.client.web.method.annotation; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/package-info.java index c28dac6fcb..63aee0b72b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/package-info.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/package-info.java @@ -17,4 +17,7 @@ /** * OAuth 2.0 Client {@code Filter}'s and supporting classes and interfaces. */ +@NullMarked package org.springframework.security.oauth2.client.web; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java index 6a60fb977a..f5e45d572e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java @@ -25,6 +25,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; @@ -118,7 +119,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_USER")); private final Mono currentAuthenticationMono = ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) + .flatMap((ctx) -> Mono.justOrEmpty(ctx.getAuthentication())) .defaultIfEmpty(ANONYMOUS_USER_TOKEN); // @formatter:off @@ -138,7 +139,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements private boolean defaultOAuth2AuthorizedClient; - private String defaultClientRegistrationId; + private @Nullable String defaultClientRegistrationId; private ClientResponseHandler clientResponseHandler; @@ -197,9 +198,14 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements ReactiveClientRegistrationRepository clientRegistrationRepository, ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler( - (clientRegistrationId, principal, attributes) -> authorizedClientRepository.removeAuthorizedClient( - clientRegistrationId, principal, - (ServerWebExchange) attributes.get(ServerWebExchange.class.getName()))); + (clientRegistrationId, principal, attributes) -> { + ServerWebExchange exchange = (ServerWebExchange) attributes.get(ServerWebExchange.class.getName()); + if (exchange != null) { + return authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, + exchange); + } + return Mono.empty(); + }); this.authorizedClientManager = createDefaultAuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository, authorizationFailureHandler); this.clientResponseHandler = new AuthorizationFailureForwarder(authorizationFailureHandler); @@ -251,7 +257,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements return (attributes) -> attributes.put(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME, authorizedClient); } - private static OAuth2AuthorizedClient oauth2AuthorizedClient(ClientRequest request) { + private static @Nullable OAuth2AuthorizedClient oauth2AuthorizedClient(ClientRequest request) { return (OAuth2AuthorizedClient) request.attributes().get(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME); } @@ -278,7 +284,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements return (attributes) -> attributes.put(SERVER_WEB_EXCHANGE_ATTR_NAME, serverWebExchange); } - private static ServerWebExchange serverWebExchange(ClientRequest request) { + private static @Nullable ServerWebExchange serverWebExchange(ClientRequest request) { return (ServerWebExchange) request.attributes().get(SERVER_WEB_EXCHANGE_ATTR_NAME); } @@ -294,7 +300,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements return ClientAttributes.clientRegistrationId(clientRegistrationId); } - private static String clientRegistrationId(ClientRequest request) { + private static @Nullable String clientRegistrationId(ClientRequest request) { OAuth2AuthorizedClient authorizedClient = oauth2AuthorizedClient(request); if (authorizedClient != null) { return authorizedClient.getClientRegistration().getRegistrationId(); @@ -376,7 +382,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements .filter(Optional::isPresent) .map(Optional::get) .flatMap(this.serverSecurityContextRepository::load) - .mapNotNull(SecurityContext::getAuthentication) + .flatMap((ctx) -> Mono.justOrEmpty(ctx.getAuthentication())) .switchIfEmpty(this.currentAuthenticationMono); // @formatter:on } @@ -541,7 +547,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements // @formatter:on } - private OAuth2Error resolveErrorIfPossible(ClientResponse response) { + private @Nullable OAuth2Error resolveErrorIfPossible(ClientResponse response) { // Try to resolve from 'WWW-Authenticate' header if (!response.headers().header(HttpHeaders.WWW_AUTHENTICATE).isEmpty()) { String wwwAuthenticateHeader = response.headers().header(HttpHeaders.WWW_AUTHENTICATE).get(0); @@ -555,7 +561,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements return resolveErrorIfPossible(response.statusCode()); } - private OAuth2Error resolveErrorIfPossible(HttpStatusCode statusCode) { + private @Nullable OAuth2Error resolveErrorIfPossible(HttpStatusCode statusCode) { if (this.httpStatusToOAuth2ErrorCodeMap.containsKey(statusCode)) { return new OAuth2Error(this.httpStatusToOAuth2ErrorCodeMap.get(statusCode), null, "https://tools.ietf.org/html/rfc6750#section-3.1"); @@ -631,7 +637,7 @@ public final class ServerOAuth2AuthorizedClientExchangeFilterFunction implements createAttributes(exchange.orElse(null))); } - private Map createAttributes(ServerWebExchange exchange) { + private Map createAttributes(@Nullable ServerWebExchange exchange) { if (exchange == null) { return Collections.emptyMap(); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index 6f7f8d3a12..c1f8363960 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -27,6 +27,7 @@ import java.util.stream.Stream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.util.context.Context; @@ -151,13 +152,13 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); - private OAuth2AuthorizedClientManager authorizedClientManager; + private @Nullable OAuth2AuthorizedClientManager authorizedClientManager; private boolean defaultOAuth2AuthorizedClient; - private String defaultClientRegistrationId; + private @Nullable String defaultClientRegistrationId; - private ClientResponseHandler clientResponseHandler; + private @Nullable ClientResponseHandler clientResponseHandler; public ServletOAuth2AuthorizedClientExchangeFilterFunction() { } @@ -224,7 +225,9 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement String clientRegistrationId, Authentication principal, Map attributes) { HttpServletRequest request = getRequest(attributes); HttpServletResponse response = getResponse(attributes); - authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, request, response); + if (request != null && response != null) { + authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, request, response); + } } /** @@ -377,12 +380,17 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement // @formatter:off return mergeRequestAttributesIfNecessary(request) .filter((req) -> req.attribute(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME).isPresent()) - .flatMap((req) -> reauthorizeClient(getOAuth2AuthorizedClient(req.attributes()), req)) + .flatMap((req) -> { + OAuth2AuthorizedClient authorizedClient = getOAuth2AuthorizedClient(req.attributes()); + return (authorizedClient != null) ? reauthorizeClient(authorizedClient, req) : Mono.empty(); + }) .switchIfEmpty( Mono.defer(() -> mergeRequestAttributesIfNecessary(request) - .filter((req) -> resolveClientRegistrationId(req) != null) - .flatMap((req) -> authorizeClient(resolveClientRegistrationId(req), req)) + .flatMap((req) -> { + String clientRegistrationId = resolveClientRegistrationId(req); + return (clientRegistrationId != null) ? authorizeClient(clientRegistrationId, req) : Mono.empty(); + }) ) ) .map((authorizedClient) -> bearer(request, authorizedClient)) @@ -392,8 +400,12 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement } private Mono exchangeAndHandleResponse(ClientRequest request, ExchangeFunction next) { - return next.exchange(request) - .transform((responseMono) -> this.clientResponseHandler.handleResponse(request, responseMono)); + return next.exchange(request).transform((responseMono) -> { + if (this.clientResponseHandler != null) { + return this.clientResponseHandler.handleResponse(request, responseMono); + } + return responseMono; + }); } private Mono mergeRequestAttributesIfNecessary(ClientRequest request) { @@ -453,7 +465,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, authentication); } - private String resolveClientRegistrationId(ClientRequest request) { + private @Nullable String resolveClientRegistrationId(ClientRequest request) { Map attrs = request.attributes(); String clientRegistrationId = getClientRegistrationId(attrs); if (clientRegistrationId == null) { @@ -468,7 +480,8 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement } private Mono authorizeClient(String clientRegistrationId, ClientRequest request) { - if (this.authorizedClientManager == null) { + OAuth2AuthorizedClientManager authorizedClientManager = this.authorizedClientManager; + if (authorizedClientManager == null) { return Mono.empty(); } Map attrs = request.attributes(); @@ -478,6 +491,9 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement } HttpServletRequest servletRequest = getRequest(attrs); HttpServletResponse servletResponse = getResponse(attrs); + if (servletRequest == null || servletResponse == null) { + return Mono.empty(); + } OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId) .principal(authentication); builder.attributes((attributes) -> addToAttributes(attributes, servletRequest, servletResponse)); @@ -485,14 +501,15 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement // NOTE: 'authorizedClientManager.authorize()' needs to be executed on a dedicated // thread via subscribeOn(Schedulers.boundedElastic()) since it performs a // blocking I/O operation using RestClient internally - return Mono.fromSupplier(() -> this.authorizedClientManager.authorize(authorizeRequest)) + return Mono.fromSupplier(() -> authorizedClientManager.authorize(authorizeRequest)) .subscribeOn(Schedulers.boundedElastic()); } private Mono reauthorizeClient(OAuth2AuthorizedClient authorizedClient, ClientRequest request) { - if (this.authorizedClientManager == null) { - return Mono.just(authorizedClient); + OAuth2AuthorizedClientManager authorizedClientManager = this.authorizedClientManager; + if (authorizedClientManager == null) { + return Mono.empty(); } Map attrs = request.attributes(); Authentication authentication = getAuthentication(attrs); @@ -501,6 +518,9 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement } HttpServletRequest servletRequest = getRequest(attrs); HttpServletResponse servletResponse = getResponse(attrs); + if (servletRequest == null || servletResponse == null) { + return Mono.just(authorizedClient); + } OAuth2AuthorizeRequest.Builder builder = OAuth2AuthorizeRequest.withAuthorizedClient(authorizedClient) .principal(authentication); builder.attributes((attributes) -> addToAttributes(attributes, servletRequest, servletResponse)); @@ -508,7 +528,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement // NOTE: 'authorizedClientManager.authorize()' needs to be executed on a dedicated // thread via subscribeOn(Schedulers.boundedElastic()) since it performs a // blocking I/O operation using RestClient internally - return Mono.fromSupplier(() -> this.authorizedClientManager.authorize(reauthorizeRequest)) + return Mono.fromSupplier(() -> authorizedClientManager.authorize(reauthorizeRequest)) .subscribeOn(Schedulers.boundedElastic()); } @@ -531,23 +551,23 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement // @formatter:on } - static OAuth2AuthorizedClient getOAuth2AuthorizedClient(Map attrs) { + static @Nullable OAuth2AuthorizedClient getOAuth2AuthorizedClient(Map attrs) { return (OAuth2AuthorizedClient) attrs.get(OAUTH2_AUTHORIZED_CLIENT_ATTR_NAME); } - static String getClientRegistrationId(Map attrs) { + static @Nullable String getClientRegistrationId(Map attrs) { return ClientAttributes.resolveClientRegistrationId(attrs); } - static Authentication getAuthentication(Map attrs) { + static @Nullable Authentication getAuthentication(Map attrs) { return (Authentication) attrs.get(AUTHENTICATION_ATTR_NAME); } - static HttpServletRequest getRequest(Map attrs) { + static @Nullable HttpServletRequest getRequest(Map attrs) { return (HttpServletRequest) attrs.get(HTTP_SERVLET_REQUEST_ATTR_NAME); } - static HttpServletResponse getResponse(Map attrs) { + static @Nullable HttpServletResponse getResponse(Map attrs) { return (HttpServletResponse) attrs.get(HTTP_SERVLET_RESPONSE_ATTR_NAME); } @@ -631,7 +651,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement // @formatter:on } - private OAuth2Error resolveErrorIfPossible(ClientResponse response) { + private @Nullable OAuth2Error resolveErrorIfPossible(ClientResponse response) { // Try to resolve from 'WWW-Authenticate' header if (!response.headers().header(HttpHeaders.WWW_AUTHENTICATE).isEmpty()) { String wwwAuthenticateHeader = response.headers().header(HttpHeaders.WWW_AUTHENTICATE).get(0); @@ -645,7 +665,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement return resolveErrorIfPossible(response.statusCode()); } - private OAuth2Error resolveErrorIfPossible(HttpStatusCode statusCode) { + private @Nullable OAuth2Error resolveErrorIfPossible(HttpStatusCode statusCode) { if (this.httpStatusToOAuth2ErrorCodeMap.containsKey(statusCode)) { return new OAuth2Error(this.httpStatusToOAuth2ErrorCodeMap.get(statusCode), null, "https://tools.ietf.org/html/rfc6750#section-3.1"); @@ -730,7 +750,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement * {@link OAuth2AuthorizationFailureHandler} completes */ private Mono handleAuthorizationFailure(OAuth2AuthorizationException exception, Authentication principal, - HttpServletRequest servletRequest, HttpServletResponse servletResponse) { + @Nullable HttpServletRequest servletRequest, @Nullable HttpServletResponse servletResponse) { Runnable runnable = () -> this.authorizationFailureHandler.onAuthorizationFailure(exception, principal, createAttributes(servletRequest, servletResponse)); // @formatter:off @@ -740,8 +760,8 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement // @formatter:on } - private static Map createAttributes(HttpServletRequest servletRequest, - HttpServletResponse servletResponse) { + private static Map createAttributes(@Nullable HttpServletRequest servletRequest, + @Nullable HttpServletResponse servletResponse) { Map attributes = new HashMap<>(); attributes.put(HttpServletRequest.class.getName(), servletRequest); attributes.put(HttpServletResponse.class.getName(), servletResponse); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/package-info.java new file mode 100644 index 0000000000..9f941ce011 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Exchange filter functions for OAuth2 Client with WebClient. + */ +@NullMarked +package org.springframework.security.oauth2.client.web.reactive.function.client; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/support/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/support/package-info.java new file mode 100644 index 0000000000..ec3e9287bc --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/support/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Support classes (reactive) for OAuth2 Client with WebClient. + */ +@NullMarked +package org.springframework.security.oauth2.client.web.reactive.function.client.support; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index 98d3dc2af9..67c669bdb7 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client.web.reactive.result.method.annotation; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; @@ -24,7 +25,6 @@ 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.ReactiveSecurityContextHolder; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; @@ -105,13 +105,16 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth return Mono.defer(() -> { RegisteredOAuth2AuthorizedClient authorizedClientAnnotation = AnnotatedElementUtils .findMergedAnnotation(parameter.getParameter(), RegisteredOAuth2AuthorizedClient.class); + if (authorizedClientAnnotation == null) { + return Mono.empty(); + } String clientRegistrationId = StringUtils.hasLength(authorizedClientAnnotation.registrationId()) ? authorizedClientAnnotation.registrationId() : null; return authorizeRequest(clientRegistrationId, exchange).flatMap(this.authorizedClientManager::authorize); }); } - private Mono authorizeRequest(String registrationId, ServerWebExchange exchange) { + private Mono authorizeRequest(@Nullable String registrationId, ServerWebExchange exchange) { Mono defaultedAuthentication = currentAuthentication(); Mono defaultedRegistrationId = Mono.justOrEmpty(registrationId) .switchIfEmpty(clientRegistrationId(defaultedAuthentication)) @@ -129,7 +132,7 @@ public final class OAuth2AuthorizedClientArgumentResolver implements HandlerMeth private Mono currentAuthentication() { // @formatter:off return ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) + .flatMap((ctx) -> Mono.justOrEmpty(ctx.getAuthentication())) .defaultIfEmpty(ANONYMOUS_USER_TOKEN); // @formatter:on } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/package-info.java new file mode 100644 index 0000000000..4cb145475c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * Method annotation support (reactive) for OAuth2 client. + */ +@NullMarked +package org.springframework.security.oauth2.client.web.reactive.result.method.annotation; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java index 5682732e5d..f208f109d0 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolver.java @@ -22,6 +22,7 @@ import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; import reactor.core.publisher.Mono; @@ -130,8 +131,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolver implements ServerOA .matches(exchange) .filter((matchResult) -> matchResult.isMatch()) .map(ServerWebExchangeMatcher.MatchResult::getVariables) - .map((variables) -> variables.get(DEFAULT_REGISTRATION_ID_URI_VARIABLE_NAME)) - .cast(String.class) + .flatMap((variables) -> Mono + .justOrEmpty((String) variables.get(DEFAULT_REGISTRATION_ID_URI_VARIABLE_NAME))) .flatMap((clientRegistrationId) -> resolve(exchange, clientRegistrationId)); // @formatter:on } @@ -167,9 +168,13 @@ public class DefaultServerOAuth2AuthorizationRequestResolver implements ServerOA ClientRegistration clientRegistration) { OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration); String redirectUriStr = expandRedirectUri(exchange.getRequest(), clientRegistration); + + String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri(); + Assert.hasText(authorizationUri, "Authorization URI is required"); + // @formatter:off builder.clientId(clientRegistration.getClientId()) - .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .authorizationUri(authorizationUri) .redirectUri(redirectUriStr) .scopes(clientRegistration.getScopes()) .state(DEFAULT_STATE_GENERATOR.generateKey()); @@ -255,7 +260,7 @@ public class DefaultServerOAuth2AuthorizationRequestResolver implements ServerOA } uriVariables.put("action", action); // @formatter:off - return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUri()) + return UriComponentsBuilder.fromUriString(Objects.requireNonNull(clientRegistration.getRedirectUri())) .buildAndExpand(uriVariables) .toUriString(); // @formatter:on diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java index 06dc334b68..58b89a3329 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.security.authentication.AnonymousAuthenticationToken; @@ -31,7 +32,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.ReactiveSecurityContextHolder; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; @@ -232,13 +232,14 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter { private Mono onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) { OAuth2AuthorizationCodeAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeAuthenticationToken) authentication; + Assert.notNull(authenticationResult.getAccessToken(), "accessToken cannot be null"); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), authenticationResult.getName(), authenticationResult.getAccessToken(), authenticationResult.getRefreshToken()); // @formatter:off return this.authenticationSuccessHandler.onAuthenticationSuccess(webFilterExchange, authentication) .then(ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) + .flatMap((ctx) -> Mono.justOrEmpty(ctx.getAuthentication())) .defaultIfEmpty(this.anonymousToken) .flatMap((principal) -> this.authorizedClientRepository .saveAuthorizedClient(authorizedClient, principal, webFilterExchange.getExchange()) @@ -255,14 +256,16 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter { ) .flatMap((exch) -> this.authorizationRequestRepository.loadAuthorizationRequest(exchange) .flatMap((authorizationRequest) -> matchesRedirectUri(exch.getRequest().getURI(), - authorizationRequest.getRedirectUri())) - ) + authorizationRequest.getRedirectUri()))) .switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch()); // @formatter:on } private static Mono matchesRedirectUri(URI authorizationResponseUri, - String authorizationRequestRedirectUri) { + @Nullable String authorizationRequestRedirectUri) { + if (authorizationRequestRedirectUri == null) { + return ServerWebExchangeMatcher.MatchResult.notMatch(); + } UriComponents requestUri = UriComponentsBuilder.fromUri(authorizationResponseUri).build(); UriComponents redirectUri = UriComponentsBuilder.fromUriString(authorizationRequestRedirectUri).build(); Set>> requestUriParameters = new LinkedHashSet<>( diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationResponseUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationResponseUtils.java index 327b7193a7..90849e6f3d 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationResponseUtils.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationResponseUtils.java @@ -67,18 +67,30 @@ final class OAuth2AuthorizationResponseUtils { String errorCode = request.getFirst(OAuth2ParameterNames.ERROR); String state = request.getFirst(OAuth2ParameterNames.STATE); if (StringUtils.hasText(code)) { - return OAuth2AuthorizationResponse.success(code).redirectUri(redirectUri).state(state).build(); + OAuth2AuthorizationResponse.Builder builder = OAuth2AuthorizationResponse.success(code) + .redirectUri(redirectUri); + if (state != null) { + builder.state(state); + } + return builder.build(); + } + if (!StringUtils.hasText(errorCode)) { + errorCode = "unknown_error"; } String errorDescription = request.getFirst(OAuth2ParameterNames.ERROR_DESCRIPTION); String errorUri = request.getFirst(OAuth2ParameterNames.ERROR_URI); - // @formatter:off - return OAuth2AuthorizationResponse.error(errorCode) - .redirectUri(redirectUri) - .errorDescription(errorDescription) - .errorUri(errorUri) - .state(state) - .build(); - // @formatter:on + OAuth2AuthorizationResponse.Builder builder = OAuth2AuthorizationResponse.error(errorCode) + .redirectUri(redirectUri); + if (errorDescription != null) { + builder.errorDescription(errorDescription); + } + if (errorUri != null) { + builder.errorUri(errorUri); + } + if (state != null) { + builder.state(state); + } + return builder.build(); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/WebSessionOAuth2ServerAuthorizationRequestRepository.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/WebSessionOAuth2ServerAuthorizationRequestRepository.java index a4319c5f07..87814f2806 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/WebSessionOAuth2ServerAuthorizationRequestRepository.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/WebSessionOAuth2ServerAuthorizationRequestRepository.java @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web.server; import java.util.Map; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -55,7 +56,7 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository // @formatter:off return getSessionAttributes(exchange) .filter((sessionAttrs) -> sessionAttrs.containsKey(this.sessionAttributeName)) - .map(this::getAuthorizationRequest) + .flatMap((sessionAttrs) -> Mono.justOrEmpty(getAuthorizationRequest(sessionAttrs))) .filter((authorizationRequest) -> state.equals(authorizationRequest.getState())); // @formatter:on } @@ -85,8 +86,9 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository return getSessionAttributes(exchange) .filter((sessionAttrs) -> sessionAttrs.containsKey(this.sessionAttributeName)) .flatMap((sessionAttrs) -> { - OAuth2AuthorizationRequest authorizationRequest = (OAuth2AuthorizationRequest) sessionAttrs.get(this.sessionAttributeName); - if (state.equals(authorizationRequest.getState())) { + OAuth2AuthorizationRequest authorizationRequest = (OAuth2AuthorizationRequest) sessionAttrs + .get(this.sessionAttributeName); + if (authorizationRequest != null && state.equals(authorizationRequest.getState())) { sessionAttrs.remove(this.sessionAttributeName); return Mono.just(authorizationRequest); } @@ -100,7 +102,7 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository * @param exchange the exchange to use * @return the state parameter or null if not found */ - private String getStateParameter(ServerWebExchange exchange) { + private @Nullable String getStateParameter(ServerWebExchange exchange) { Assert.notNull(exchange, "exchange cannot be null"); return exchange.getRequest().getQueryParams().getFirst(OAuth2ParameterNames.STATE); } @@ -109,7 +111,7 @@ public final class WebSessionOAuth2ServerAuthorizationRequestRepository return exchange.getSession().map(WebSession::getAttributes); } - private OAuth2AuthorizationRequest getAuthorizationRequest(Map sessionAttrs) { + private @Nullable OAuth2AuthorizationRequest getAuthorizationRequest(Map sessionAttrs) { return (OAuth2AuthorizationRequest) sessionAttrs.get(this.sessionAttributeName); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/authentication/OAuth2LoginAuthenticationWebFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/authentication/OAuth2LoginAuthenticationWebFilter.java index 160be1b315..0059df58e0 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/authentication/OAuth2LoginAuthenticationWebFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/authentication/OAuth2LoginAuthenticationWebFilter.java @@ -55,6 +55,8 @@ public class OAuth2LoginAuthenticationWebFilter extends AuthenticationWebFilter @Override protected Mono onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) { OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) authentication; + Assert.notNull(authenticationResult.getAccessToken(), "accessToken cannot be null"); + Assert.notNull(authenticationResult.getPrincipal(), "principal cannot be null"); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( authenticationResult.getClientRegistration(), authenticationResult.getName(), authenticationResult.getAccessToken(), authenticationResult.getRefreshToken()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/authentication/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/authentication/package-info.java new file mode 100644 index 0000000000..ab313ed3c4 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/authentication/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * OAuth 2.0 Client {@code WebFilter}'s and supporting classes and interfaces. + */ +@NullMarked +package org.springframework.security.oauth2.client.web.server.authentication; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/package-info.java new file mode 100644 index 0000000000..bc0d8d8a31 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present 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. + */ + +/** + * OAuth 2.0 Client {@code WebFilter}'s and supporting classes and interfaces. + */ +@NullMarked +package org.springframework.security.oauth2.client.web.server; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index a7e6d3c165..8a029e16bd 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -662,12 +662,6 @@ public class ClientRegistrationTests { assertThat(clientRegistration.getClientAuthenticationMethod()).isEqualTo(clientAuthenticationMethod); } - @Test - void clientSettingsWhenNullThenThrowIllegalArgumentException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> ClientRegistration.withRegistrationId(REGISTRATION_ID).clientSettings(null)); - } - // gh-16382 @Test void buildWhenDefaultClientSettingsThenDefaulted() { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java index 053743129b..1d037a211e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java @@ -166,7 +166,7 @@ public class ClientRegistrationsTests { assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); assertThat(registration.getRegistrationId()).isEqualTo(URI.create(this.issuer).getHost()); assertThat(registration.getClientName()).isEqualTo(this.issuer); - assertThat(registration.getScopes()).isNull(); + assertThat(registration.getScopes()).isEmpty(); assertThat(provider.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth"); assertThat(provider.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token"); assertThat(provider.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessorWebClientTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessorWebClientTests.java index 6db16919b5..827e40a9d1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessorWebClientTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/client/ClientRegistrationIdProcessorWebClientTests.java @@ -16,12 +16,16 @@ package org.springframework.security.oauth2.client.web.client; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; @@ -69,7 +73,13 @@ class ClientRegistrationIdProcessorWebClientTests extends AbstractMockServerClie ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction( authorizedClientManager); - WebClient.Builder builder = WebClient.builder().filter(oauth2Client).baseUrl(this.baseUrl); + WebClient.Builder builder = WebClient.builder() + .defaultRequest((requestSpec) -> requestSpec.attributes((attrs) -> { + attrs.put(HttpServletRequest.class.getName(), new MockHttpServletRequest()); + attrs.put(HttpServletResponse.class.getName(), new MockHttpServletResponse()); + })) + .filter(oauth2Client) + .baseUrl(this.baseUrl); ArgumentCaptor authorizeRequest = ArgumentCaptor.forClass(OAuth2AuthorizeRequest.class); given(authorizedClientManager.authorize(authorizeRequest.capture())).willReturn(this.authorizedClient); diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java index f45361382c..bc7224e008 100644 --- a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java +++ b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java @@ -35,9 +35,11 @@ import reactor.core.publisher.Mono; import org.springframework.context.ApplicationContext; import org.springframework.core.convert.converter.Converter; import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; @@ -1154,7 +1156,9 @@ public final class SecurityMockServerConfigurers { OAuth2ClientServerTestUtils.setAuthorizedClientRepository(exchange, authorizedClientRepository); } TestOAuth2AuthorizedClientRepository.enable(exchange); - return authorizedClientRepository.saveAuthorizedClient(client, null, exchange) + Authentication anonymousPrincipal = new AnonymousAuthenticationToken("anonymous", "anonymousUser", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + return authorizedClientRepository.saveAuthorizedClient(client, anonymousPrincipal, exchange) .then(chain.filter(exchange)); }); } @@ -1195,9 +1199,7 @@ public final class SecurityMockServerConfigurers { @Override public Mono authorize(OAuth2AuthorizeRequest authorizeRequest) { ServerWebExchange exchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName()); - if (isEnabled(exchange)) { - Assert.isTrue(this.authorizedClientRepository != null, - "ServerOAuth2AuthorizedClientRepository not set"); + if (exchange != null && isEnabled(exchange) && this.authorizedClientRepository != null) { return this.authorizedClientRepository.loadAuthorizedClient( authorizeRequest.getClientRegistrationId(), authorizeRequest.getPrincipal(), exchange); } diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java index a961512bd9..8aba494fad 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java @@ -1634,7 +1634,10 @@ public final class SecurityMockMvcRequestPostProcessors { OAuth2ClientServletTestUtils.setAuthorizedClientRepository(request, authorizedClientRepository); } TestOAuth2AuthorizedClientRepository.enable(request); - authorizedClientRepository.saveAuthorizedClient(client, null, request, new MockHttpServletResponse()); + Authentication anonymousPrincipal = new AnonymousAuthenticationToken("anonymous", "anonymousUser", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + authorizedClientRepository.saveAuthorizedClient(client, anonymousPrincipal, request, + new MockHttpServletResponse()); return request; } @@ -1668,7 +1671,7 @@ public final class SecurityMockMvcRequestPostProcessors { @Override public @Nullable OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest) { HttpServletRequest request = authorizeRequest.getAttribute(HttpServletRequest.class.getName()); - if (this.authorizedClientRepository != null && isEnabled(request)) { + if (request != null && isEnabled(request) && this.authorizedClientRepository != null) { return this.authorizedClientRepository.loadAuthorizedClient( authorizeRequest.getClientRegistrationId(), authorizeRequest.getPrincipal(), request); } @@ -1703,7 +1706,7 @@ public final class SecurityMockMvcRequestPostProcessors { } @Override - public T loadAuthorizedClient(String clientRegistrationId, + public @Nullable T loadAuthorizedClient(String clientRegistrationId, Authentication principal, HttpServletRequest request) { if (isEnabled(request)) { return (T) request.getAttribute(TOKEN_ATTR_NAME);