From db5310bee826aa981c8ebb13ae2ce2a03d58c8db Mon Sep 17 00:00:00 2001 From: Joe Grandja <10884212+jgrandja@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:36:33 -0500 Subject: [PATCH] Enable null-safety in spring-security-oauth2-core Closes gh-17820 --- .../spring-security-oauth2-core.gradle | 1 + .../oauth2/core/AbstractOAuth2Token.java | 13 +- .../security/oauth2/core/ClaimAccessor.java | 31 ++-- .../core/ClientAuthenticationMethod.java | 2 - .../DefaultOAuth2AuthenticatedPrincipal.java | 16 ++- .../oauth2/core/OAuth2AccessToken.java | 17 ++- .../core/OAuth2AuthenticatedPrincipal.java | 6 +- .../core/OAuth2AuthenticationException.java | 17 ++- .../security/oauth2/core/OAuth2Error.java | 20 +-- .../oauth2/core/OAuth2RefreshToken.java | 12 +- .../security/oauth2/core/OAuth2Token.java | 8 +- ...OAuth2TokenIntrospectionClaimAccessor.java | 91 ++++++------ .../core/authorization/package-info.java | 23 +++ .../converter/ClaimConversionService.java | 4 +- .../converter/ObjectToBooleanConverter.java | 4 +- .../converter/ObjectToInstantConverter.java | 4 +- .../ObjectToListStringConverter.java | 4 +- .../ObjectToMapStringObjectConverter.java | 7 +- .../converter/ObjectToStringConverter.java | 4 +- .../core/converter/ObjectToURLConverter.java | 4 +- .../oauth2/core/converter/package-info.java | 23 +++ ...MapOAuth2AccessTokenResponseConverter.java | 11 +- .../endpoint/OAuth2AccessTokenResponse.java | 50 ++++--- .../endpoint/OAuth2AuthorizationRequest.java | 59 ++++---- .../endpoint/OAuth2AuthorizationResponse.java | 39 +++--- .../OAuth2DeviceAuthorizationResponse.java | 33 +++-- .../oauth2/core/endpoint/package-info.java | 3 + .../http/converter/HttpMessageConverters.java | 4 +- ...cessTokenResponseHttpMessageConverter.java | 6 +- ...orizationResponseHttpMessageConverter.java | 23 ++- .../OAuth2ErrorHttpMessageConverter.java | 7 +- .../core/http/converter/package-info.java | 23 +++ .../oauth2/core/http/package-info.java | 23 +++ .../core/oidc/AddressStandardClaim.java | 41 +++--- .../oidc/DefaultAddressStandardClaim.java | 62 ++++---- .../core/oidc/IdTokenClaimAccessor.java | 90 +++++++----- .../oauth2/core/oidc/OidcIdToken.java | 18 ++- .../core/oidc/StandardClaimAccessor.java | 132 ++++++++++-------- .../core/oidc/endpoint/package-info.java | 3 + .../oauth2/core/oidc/package-info.java | 3 + .../core/oidc/user/DefaultOidcUser.java | 26 ++-- .../oauth2/core/oidc/user/OidcUser.java | 9 +- .../core/oidc/user/OidcUserAuthority.java | 26 ++-- .../oauth2/core/oidc/user/package-info.java | 3 + .../security/oauth2/core/package-info.java | 3 + .../oauth2/core/user/DefaultOAuth2User.java | 16 ++- .../oauth2/core/user/OAuth2UserAuthority.java | 26 ++-- .../oauth2/core/user/package-info.java | 3 + .../oauth2/core/web/package-info.java | 23 +++ .../web/reactive/function/package-info.java | 23 +++ .../core/web/reactive/package-info.java | 23 +++ .../BearerTokenAuthenticationTests.java | 4 +- 52 files changed, 735 insertions(+), 391 deletions(-) create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/package-info.java create mode 100644 oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/package-info.java diff --git a/oauth2/oauth2-core/spring-security-oauth2-core.gradle b/oauth2/oauth2-core/spring-security-oauth2-core.gradle index a3c3a2f4e9..43f7c114eb 100644 --- a/oauth2/oauth2-core/spring-security-oauth2-core.gradle +++ b/oauth2/oauth2-core/spring-security-oauth2-core.gradle @@ -1,5 +1,6 @@ plugins { id 'compile-warnings-error' + id 'security-nullability' } apply plugin: 'io.spring.convention.spring-module' diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2Token.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2Token.java index 2182f9a1cd..d06b30aa95 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2Token.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/AbstractOAuth2Token.java @@ -19,7 +19,8 @@ package org.springframework.security.oauth2.core; import java.io.Serializable; import java.time.Instant; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -37,9 +38,9 @@ public abstract class AbstractOAuth2Token implements OAuth2Token, Serializable { private final String tokenValue; - private final Instant issuedAt; + private final @Nullable Instant issuedAt; - private final Instant expiresAt; + private final @Nullable Instant expiresAt; /** * Sub-class constructor. @@ -78,8 +79,7 @@ public abstract class AbstractOAuth2Token implements OAuth2Token, Serializable { * Returns the time at which the token was issued. * @return the time the token was issued or {@code null} */ - @Nullable - public Instant getIssuedAt() { + public @Nullable Instant getIssuedAt() { return this.issuedAt; } @@ -87,8 +87,7 @@ public abstract class AbstractOAuth2Token implements OAuth2Token, Serializable { * Returns the expiration time on or after which the token MUST NOT be accepted. * @return the token expiration time or {@code null} */ - @Nullable - public Instant getExpiresAt() { + public @Nullable Instant getExpiresAt() { return this.expiresAt; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java index a42c0893e6..84d451fb72 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClaimAccessor.java @@ -21,6 +21,8 @@ import java.time.Instant; import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.security.oauth2.core.converter.ClaimConversionService; import org.springframework.util.Assert; @@ -44,11 +46,11 @@ public interface ClaimAccessor { * type {@code T}. * @param claim the name of the claim * @param the type of the claim value - * @return the claim value + * @return the claim value, or {@code null} if the claim does not exist * @since 5.2 */ @SuppressWarnings("unchecked") - default T getClaim(String claim) { + default @Nullable T getClaim(String claim) { return !hasClaim(claim) ? null : (T) getClaims().get(claim); } @@ -71,7 +73,7 @@ public interface ClaimAccessor { * @return the claim value or {@code null} if it does not exist or is equal to * {@code null} */ - default String getClaimAsString(String claim) { + default @Nullable String getClaimAsString(String claim) { return !hasClaim(claim) ? null : ClaimConversionService.getSharedInstance().convert(getClaims().get(claim), String.class); } @@ -85,7 +87,8 @@ public interface ClaimAccessor { * {@code Boolean} * @throws NullPointerException if the claim value is {@code null} */ - default Boolean getClaimAsBoolean(String claim) { + @SuppressWarnings("NullAway") + default @Nullable Boolean getClaimAsBoolean(String claim) { if (!hasClaim(claim)) { return null; } @@ -100,8 +103,12 @@ public interface ClaimAccessor { * Returns the claim value as an {@code Instant} or {@code null} if it does not exist. * @param claim the name of the claim * @return the claim value or {@code null} if it does not exist + * @throws IllegalArgumentException if the claim value cannot be converted to an + * {@code Instant} + * @throws NullPointerException if the claim value is {@code null} */ - default Instant getClaimAsInstant(String claim) { + @SuppressWarnings("NullAway") + default @Nullable Instant getClaimAsInstant(String claim) { if (!hasClaim(claim)) { return null; } @@ -116,8 +123,12 @@ public interface ClaimAccessor { * Returns the claim value as an {@code URL} or {@code null} if it does not exist. * @param claim the name of the claim * @return the claim value or {@code null} if it does not exist + * @throws IllegalArgumentException if the claim value cannot be converted to a + * {@code URL} + * @throws NullPointerException if the claim value is {@code null} */ - default URL getClaimAsURL(String claim) { + @SuppressWarnings("NullAway") + default @Nullable URL getClaimAsURL(String claim) { if (!hasClaim(claim)) { return null; } @@ -137,8 +148,8 @@ public interface ClaimAccessor { * {@code Map} * @throws NullPointerException if the claim value is {@code null} */ - @SuppressWarnings("unchecked") - default Map getClaimAsMap(String claim) { + @SuppressWarnings({ "unchecked", "NullAway" }) + default @Nullable Map getClaimAsMap(String claim) { if (!hasClaim(claim)) { return null; } @@ -162,8 +173,8 @@ public interface ClaimAccessor { * {@code List} * @throws NullPointerException if the claim value is {@code null} */ - @SuppressWarnings("unchecked") - default List getClaimAsStringList(String claim) { + @SuppressWarnings({ "unchecked", "NullAway" }) + default @Nullable List getClaimAsStringList(String claim) { if (!hasClaim(claim)) { return null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java index 6255f13b8b..cdb13b3a4f 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/ClientAuthenticationMethod.java @@ -18,7 +18,6 @@ package org.springframework.security.oauth2.core; import java.io.Serializable; -import org.springframework.lang.NonNull; import org.springframework.util.Assert; /** @@ -105,7 +104,6 @@ public final class ClientAuthenticationMethod implements Serializable { * constant, if any * @since 6.5 */ - @NonNull public static ClientAuthenticationMethod valueOf(String method) { for (ClientAuthenticationMethod m : methods()) { if (m.getValue().equals(method)) { diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DefaultOAuth2AuthenticatedPrincipal.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DefaultOAuth2AuthenticatedPrincipal.java index 6de624f633..5adc48aaf7 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DefaultOAuth2AuthenticatedPrincipal.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/DefaultOAuth2AuthenticatedPrincipal.java @@ -22,6 +22,8 @@ import java.util.Collection; import java.util.Collections; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.util.Assert; @@ -58,17 +60,21 @@ public final class DefaultOAuth2AuthenticatedPrincipal implements OAuth2Authenti /** * Constructs an {@code DefaultOAuth2AuthenticatedPrincipal} using the provided * parameters. - * @param name the name attached to the OAuth 2.0 token + * @param name the name attached to the OAuth 2.0 token, may be {@code null} * @param attributes the attributes of the OAuth 2.0 token - * @param authorities the authorities of the OAuth 2.0 token + * @param authorities the authorities of the OAuth 2.0 token, may be {@code null} */ - public DefaultOAuth2AuthenticatedPrincipal(String name, Map attributes, - Collection authorities) { + public DefaultOAuth2AuthenticatedPrincipal(@Nullable String name, Map attributes, + @Nullable Collection authorities) { Assert.notEmpty(attributes, "attributes cannot be empty"); this.attributes = Collections.unmodifiableMap(attributes); this.authorities = (authorities != null) ? Collections.unmodifiableCollection(authorities) : AuthorityUtils.NO_AUTHORITIES; - this.name = (name != null) ? name : (String) this.attributes.get("sub"); + // Ensure name is never null - use 'sub' attribute as fallback, then empty string + // This satisfies AuthenticatedPrincipal.getName() contract which never returns + // null + String resolvedName = (name != null) ? name : (String) this.attributes.get("sub"); + this.name = (resolvedName != null) ? resolvedName : ""; } /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java index 9174a44654..ef0dbe4ccf 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AccessToken.java @@ -22,6 +22,8 @@ import java.time.Instant; import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -52,11 +54,12 @@ public class OAuth2AccessToken extends AbstractOAuth2Token { * Constructs an {@code OAuth2AccessToken} using the provided parameters. * @param tokenType the token type * @param tokenValue the token value - * @param issuedAt the time at which the token was issued + * @param issuedAt the time at which the token was issued, may be {@code null} * @param expiresAt the expiration time on or after which the token MUST NOT be - * accepted + * accepted, may be {@code null} */ - public OAuth2AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt) { + public OAuth2AccessToken(TokenType tokenType, String tokenValue, @Nullable Instant issuedAt, + @Nullable Instant expiresAt) { this(tokenType, tokenValue, issuedAt, expiresAt, Collections.emptySet()); } @@ -64,13 +67,13 @@ public class OAuth2AccessToken extends AbstractOAuth2Token { * Constructs an {@code OAuth2AccessToken} using the provided parameters. * @param tokenType the token type * @param tokenValue the token value - * @param issuedAt the time at which the token was issued + * @param issuedAt the time at which the token was issued, may be {@code null} * @param expiresAt the expiration time on or after which the token MUST NOT be - * accepted + * accepted, may be {@code null} * @param scopes the scope(s) associated to the token */ - public OAuth2AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt, - Set scopes) { + public OAuth2AccessToken(TokenType tokenType, String tokenValue, @Nullable Instant issuedAt, + @Nullable Instant expiresAt, Set scopes) { super(tokenValue, issuedAt, expiresAt); Assert.notNull(tokenType, "tokenType cannot be null"); this.tokenType = tokenType; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.java index 61b718ce95..2a23f08efa 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.java @@ -19,7 +19,8 @@ package org.springframework.security.oauth2.core; import java.util.Collection; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.core.GrantedAuthority; @@ -38,9 +39,8 @@ public interface OAuth2AuthenticatedPrincipal extends AuthenticatedPrincipal { * @param the type of the attribute * @return the attribute or {@code null} otherwise */ - @Nullable @SuppressWarnings("unchecked") - default A getAttribute(String name) { + default @Nullable A getAttribute(String name) { return (A) getAttributes().get(name); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java index 394c9d7f3f..8793c81f1d 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2AuthenticationException.java @@ -18,6 +18,8 @@ package org.springframework.security.oauth2.core; import java.io.Serial; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.util.Assert; @@ -77,22 +79,25 @@ public class OAuth2AuthenticationException extends AuthenticationException { /** * Constructs an {@code OAuth2AuthenticationException} using the provided parameters. * @param error the {@link OAuth2Error OAuth 2.0 Error} - * @param message the detail message + * @param message the detail message, may be {@code null} */ - public OAuth2AuthenticationException(OAuth2Error error, String message) { + public OAuth2AuthenticationException(OAuth2Error error, @Nullable String message) { this(error, message, null); } /** * Constructs an {@code OAuth2AuthenticationException} using the provided parameters. * @param error the {@link OAuth2Error OAuth 2.0 Error} - * @param message the detail message - * @param cause the root cause + * @param message the detail message, may be {@code null} + * @param cause the root cause, may be {@code null} */ - public OAuth2AuthenticationException(OAuth2Error error, String message, Throwable cause) { - super(message, cause); + public OAuth2AuthenticationException(OAuth2Error error, @Nullable String message, @Nullable Throwable cause) { + super(message); Assert.notNull(error, "error cannot be null"); this.error = error; + if (cause != null) { + initCause(cause); + } } /** diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java index 5aaedebef9..f4e6587c20 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Error.java @@ -18,6 +18,8 @@ package org.springframework.security.oauth2.core; import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -41,9 +43,9 @@ public class OAuth2Error implements Serializable { private final String errorCode; - private final String description; + private final @Nullable String description; - private final String uri; + private final @Nullable String uri; /** * Constructs an {@code OAuth2Error} using the provided parameters. @@ -56,10 +58,10 @@ public class OAuth2Error implements Serializable { /** * Constructs an {@code OAuth2Error} using the provided parameters. * @param errorCode the error code - * @param description the error description - * @param uri the error uri + * @param description the error description, may be {@code null} + * @param uri the error uri, may be {@code null} */ - public OAuth2Error(String errorCode, String description, String uri) { + public OAuth2Error(String errorCode, @Nullable String description, @Nullable String uri) { Assert.hasText(errorCode, "errorCode cannot be empty"); this.errorCode = errorCode; this.description = description; @@ -76,17 +78,17 @@ public class OAuth2Error implements Serializable { /** * Returns the error description. - * @return the error description + * @return the error description, or {@code null} if not available */ - public final String getDescription() { + public final @Nullable String getDescription() { return this.description; } /** * Returns the error uri. - * @return the error uri + * @return the error uri, or {@code null} if not available */ - public final String getUri() { + public final @Nullable String getUri() { return this.uri; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java index 2f04b823e9..85e9a1739b 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2RefreshToken.java @@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core; import java.io.Serial; import java.time.Instant; +import org.jspecify.annotations.Nullable; + /** * An implementation of an {@link AbstractOAuth2Token} representing an OAuth 2.0 Refresh * Token. @@ -43,20 +45,20 @@ public class OAuth2RefreshToken extends AbstractOAuth2Token { /** * Constructs an {@code OAuth2RefreshToken} using the provided parameters. * @param tokenValue the token value - * @param issuedAt the time at which the token was issued + * @param issuedAt the time at which the token was issued, may be {@code null} */ - public OAuth2RefreshToken(String tokenValue, Instant issuedAt) { + public OAuth2RefreshToken(String tokenValue, @Nullable Instant issuedAt) { this(tokenValue, issuedAt, null); } /** * Constructs an {@code OAuth2RefreshToken} using the provided parameters. * @param tokenValue the token value - * @param issuedAt the time at which the token was issued - * @param expiresAt the time at which the token expires + * @param issuedAt the time at which the token was issued, may be {@code null} + * @param expiresAt the time at which the token expires, may be {@code null} * @since 5.5 */ - public OAuth2RefreshToken(String tokenValue, Instant issuedAt, Instant expiresAt) { + public OAuth2RefreshToken(String tokenValue, @Nullable Instant issuedAt, @Nullable Instant expiresAt) { super(tokenValue, issuedAt, expiresAt); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Token.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Token.java index 00da44931f..9f263f4b7f 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Token.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2Token.java @@ -18,7 +18,7 @@ package org.springframework.security.oauth2.core; import java.time.Instant; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Core interface representing an OAuth 2.0 Token. @@ -39,8 +39,7 @@ public interface OAuth2Token { * Returns the time at which the token was issued. * @return the time the token was issued or {@code null} */ - @Nullable - default Instant getIssuedAt() { + default @Nullable Instant getIssuedAt() { return null; } @@ -48,8 +47,7 @@ public interface OAuth2Token { * Returns the expiration time on or after which the token MUST NOT be accepted. * @return the token expiration time or {@code null} */ - @Nullable - default Instant getExpiresAt() { + default @Nullable Instant getExpiresAt() { return null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java index adf8e80ed9..b68550ed89 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/OAuth2TokenIntrospectionClaimAccessor.java @@ -20,7 +20,7 @@ import java.net.URL; import java.time.Instant; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A {@link ClaimAccessor} for the "claims" that may be contained in the @@ -45,105 +45,106 @@ public interface OAuth2TokenIntrospectionClaimAccessor extends ClaimAccessor { /** * Returns a human-readable identifier {@code (username)} for the resource owner that - * authorized the token + * authorized the token, or {@code null} if it does not exist. * @return a human-readable identifier for the resource owner that authorized the - * token + * token, or {@code null} if it does not exist */ - @Nullable - default String getUsername() { + default @Nullable String getUsername() { return getClaimAsString(OAuth2TokenIntrospectionClaimNames.USERNAME); } /** - * Returns the client identifier {@code (client_id)} for the token - * @return the client identifier for the token + * Returns the client identifier {@code (client_id)} for the token, or {@code null} if + * it does not exist. + * @return the client identifier for the token, or {@code null} if it does not exist */ - @Nullable - default String getClientId() { + default @Nullable String getClientId() { return getClaimAsString(OAuth2TokenIntrospectionClaimNames.CLIENT_ID); } /** - * Returns the scopes {@code (scope)} associated with the token - * @return the scopes associated with the token + * Returns the scopes {@code (scope)} associated with the token, or {@code null} if it + * does not exist. + * @return the scopes associated with the token, or {@code null} if it does not exist */ - @Nullable - default List getScopes() { + default @Nullable List getScopes() { return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.SCOPE); } /** - * Returns the type of the token {@code (token_type)}, for example {@code bearer}. - * @return the type of the token, for example {@code bearer}. + * Returns the type of the token {@code (token_type)}, for example {@code bearer}, or + * {@code null} if it does not exist. + * @return the type of the token, for example {@code bearer}, or {@code null} if it + * does not exist */ - @Nullable - default String getTokenType() { + default @Nullable String getTokenType() { return getClaimAsString(OAuth2TokenIntrospectionClaimNames.TOKEN_TYPE); } /** - * Returns a timestamp {@code (exp)} indicating when the token expires - * @return a timestamp indicating when the token expires + * Returns a timestamp {@code (exp)} indicating when the token expires, or + * {@code null} if it does not exist. + * @return a timestamp indicating when the token expires, or {@code null} if it does + * not exist */ - @Nullable - default Instant getExpiresAt() { + default @Nullable Instant getExpiresAt() { return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.EXP); } /** - * Returns a timestamp {@code (iat)} indicating when the token was issued - * @return a timestamp indicating when the token was issued + * Returns a timestamp {@code (iat)} indicating when the token was issued, or + * {@code null} if it does not exist. + * @return a timestamp indicating when the token was issued, or {@code null} if it + * does not exist */ - @Nullable - default Instant getIssuedAt() { + default @Nullable Instant getIssuedAt() { return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.IAT); } /** * Returns a timestamp {@code (nbf)} indicating when the token is not to be used - * before - * @return a timestamp indicating when the token is not to be used before + * before, or {@code null} if it does not exist. + * @return a timestamp indicating when the token is not to be used before, or + * {@code null} if it does not exist */ - @Nullable - default Instant getNotBefore() { + default @Nullable Instant getNotBefore() { return getClaimAsInstant(OAuth2TokenIntrospectionClaimNames.NBF); } /** * Returns usually a machine-readable identifier {@code (sub)} of the resource owner - * who authorized the token + * who authorized the token, or {@code null} if it does not exist. * @return usually a machine-readable identifier of the resource owner who authorized - * the token + * the token, or {@code null} if it does not exist */ - @Nullable - default String getSubject() { + default @Nullable String getSubject() { return getClaimAsString(OAuth2TokenIntrospectionClaimNames.SUB); } /** - * Returns the intended audience {@code (aud)} for the token - * @return the intended audience for the token + * Returns the intended audience {@code (aud)} for the token, or {@code null} if it + * does not exist. + * @return the intended audience for the token, or {@code null} if it does not exist */ - @Nullable - default List getAudience() { + default @Nullable List getAudience() { return getClaimAsStringList(OAuth2TokenIntrospectionClaimNames.AUD); } /** - * Returns the issuer {@code (iss)} of the token - * @return the issuer of the token + * Returns the issuer {@code (iss)} of the token, or {@code null} if it does not + * exist. + * @return the issuer of the token, or {@code null} if it does not exist */ - @Nullable - default URL getIssuer() { + default @Nullable URL getIssuer() { return getClaimAsURL(OAuth2TokenIntrospectionClaimNames.ISS); } /** - * Returns the identifier {@code (jti)} for the token - * @return the identifier for the token + * Returns the identifier {@code (jti)} for the token, or {@code null} if it does not + * exist. + * @return the identifier for the token, or {@code null} if it does not exist */ - @Nullable - default String getId() { + default @Nullable String getId() { return getClaimAsString(OAuth2TokenIntrospectionClaimNames.JTI); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/package-info.java new file mode 100644 index 0000000000..7afef53466 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/authorization/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 that provide OAuth 2.0 authorization managers. + */ +@NullMarked +package org.springframework.security.oauth2.core.authorization; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ClaimConversionService.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ClaimConversionService.java index 9ca8ecb93f..9594dbe58f 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ClaimConversionService.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ClaimConversionService.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.core.converter; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.support.GenericConversionService; @@ -32,7 +34,7 @@ import org.springframework.security.oauth2.core.ClaimAccessor; */ public final class ClaimConversionService extends GenericConversionService { - private static volatile ClaimConversionService sharedInstance; + private static volatile @Nullable ClaimConversionService sharedInstance; private ClaimConversionService() { addConverters(this); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToBooleanConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToBooleanConverter.java index a86350ab05..ce9d19fb00 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToBooleanConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToBooleanConverter.java @@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core.converter; import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -34,7 +36,7 @@ final class ObjectToBooleanConverter implements GenericConverter { } @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToInstantConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToInstantConverter.java index 8f1d9f1a91..ea7f1cc951 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToInstantConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToInstantConverter.java @@ -21,6 +21,8 @@ import java.util.Collections; import java.util.Date; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -36,7 +38,7 @@ final class ObjectToInstantConverter implements GenericConverter { } @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToListStringConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToListStringConverter.java index 055ec75519..fdd2a4a09b 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToListStringConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToListStringConverter.java @@ -23,6 +23,8 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.util.ClassUtils; @@ -49,7 +51,7 @@ final class ObjectToListStringConverter implements ConditionalGenericConverter { } @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToMapStringObjectConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToMapStringObjectConverter.java index b2bbcb12e1..23dc633aea 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToMapStringObjectConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToMapStringObjectConverter.java @@ -21,6 +21,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; @@ -37,12 +39,13 @@ final class ObjectToMapStringObjectConverter implements ConditionalGenericConver @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + TypeDescriptor mapKeyTypeDescriptor = targetType.getMapKeyTypeDescriptor(); return targetType.getElementTypeDescriptor() == null - || targetType.getMapKeyTypeDescriptor().getType().equals(String.class); + || (mapKeyTypeDescriptor != null && mapKeyTypeDescriptor.getType().equals(String.class)); } @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToStringConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToStringConverter.java index 4f1fd43c6c..3c415063a4 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToStringConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToStringConverter.java @@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core.converter; import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -34,7 +36,7 @@ final class ObjectToStringConverter implements GenericConverter { } @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return (source != null) ? source.toString() : null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToURLConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToURLConverter.java index c9ee6511f5..46e3a1809e 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToURLConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/ObjectToURLConverter.java @@ -21,6 +21,8 @@ import java.net.URL; import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; @@ -36,7 +38,7 @@ final class ObjectToURLConverter implements GenericConverter { } @Override - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/package-info.java new file mode 100644 index 0000000000..24fed93719 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/converter/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 that provide claim type converters for OAuth 2.0 and OpenID Connect. + */ +@NullMarked +package org.springframework.security.oauth2.core.converter; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverter.java index 52cc9ae096..52a2e0c062 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/DefaultMapOAuth2AccessTokenResponseConverter.java @@ -23,6 +23,8 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.util.StringUtils; @@ -44,6 +46,9 @@ public final class DefaultMapOAuth2AccessTokenResponseConverter @Override public OAuth2AccessTokenResponse convert(Map source) { String accessToken = getParameterValue(source, OAuth2ParameterNames.ACCESS_TOKEN); + if (accessToken == null) { + throw new IllegalArgumentException("Missing required parameter: " + OAuth2ParameterNames.ACCESS_TOKEN); + } OAuth2AccessToken.TokenType accessTokenType = getAccessTokenType(source); long expiresIn = getExpiresIn(source); Set scopes = getScopes(source); @@ -65,7 +70,8 @@ public final class DefaultMapOAuth2AccessTokenResponseConverter // @formatter:on } - private static OAuth2AccessToken.TokenType getAccessTokenType(Map tokenResponseParameters) { + private static OAuth2AccessToken.@Nullable TokenType getAccessTokenType( + Map tokenResponseParameters) { if (OAuth2AccessToken.TokenType.BEARER.getValue() .equalsIgnoreCase(getParameterValue(tokenResponseParameters, OAuth2ParameterNames.TOKEN_TYPE))) { return OAuth2AccessToken.TokenType.BEARER; @@ -89,7 +95,8 @@ public final class DefaultMapOAuth2AccessTokenResponseConverter return Collections.emptySet(); } - private static String getParameterValue(Map tokenResponseParameters, String parameterName) { + private static @Nullable String getParameterValue(Map tokenResponseParameters, + String parameterName) { Object obj = tokenResponseParameters.get(parameterName); return (obj != null) ? obj.toString() : null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java index 4e0fe2e276..3a9b56c70f 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java @@ -21,9 +21,11 @@ import java.util.Collections; import java.util.Map; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -39,11 +41,11 @@ import org.springframework.util.StringUtils; */ public final class OAuth2AccessTokenResponse { - private OAuth2AccessToken accessToken; + private @Nullable OAuth2AccessToken accessToken; - private OAuth2RefreshToken refreshToken; + private @Nullable OAuth2RefreshToken refreshToken; - private Map additionalParameters; + private @Nullable Map additionalParameters; private OAuth2AccessTokenResponse() { } @@ -53,12 +55,14 @@ public final class OAuth2AccessTokenResponse { * @return the {@link OAuth2AccessToken} */ public OAuth2AccessToken getAccessToken() { + Assert.notNull(this.accessToken, "accessToken cannot be null"); return this.accessToken; } /** * Returns the {@link OAuth2RefreshToken Refresh Token}. - * @return the {@link OAuth2RefreshToken} + * @return the {@link OAuth2RefreshToken}, or {@code null} if not present in the + * response * @since 5.1 */ public @Nullable OAuth2RefreshToken getRefreshToken() { @@ -71,6 +75,7 @@ public final class OAuth2AccessTokenResponse { * empty. */ public Map getAdditionalParameters() { + Assert.notNull(this.additionalParameters, "additionalParameters cannot be null"); return this.additionalParameters; } @@ -99,19 +104,19 @@ public final class OAuth2AccessTokenResponse { private String tokenValue; - private OAuth2AccessToken.TokenType tokenType; + private OAuth2AccessToken.@Nullable TokenType tokenType; - private Instant issuedAt; + private @Nullable Instant issuedAt; - private Instant expiresAt; + private @Nullable Instant expiresAt; private long expiresIn; - private Set scopes; + private @Nullable Set scopes; - private String refreshToken; + private @Nullable String refreshToken; - private Map additionalParameters; + private @Nullable Map additionalParameters; private Builder(OAuth2AccessTokenResponse response) { OAuth2AccessToken accessToken = response.getAccessToken(); @@ -131,10 +136,10 @@ public final class OAuth2AccessTokenResponse { /** * Sets the {@link OAuth2AccessToken.TokenType token type}. - * @param tokenType the type of token issued + * @param tokenType the type of token issued, may be {@code null} * @return the {@link Builder} */ - public Builder tokenType(OAuth2AccessToken.TokenType tokenType) { + public Builder tokenType(OAuth2AccessToken.@Nullable TokenType tokenType) { this.tokenType = tokenType; return this; } @@ -152,30 +157,32 @@ public final class OAuth2AccessTokenResponse { /** * Sets the scope(s) associated to the access token. - * @param scopes the scope(s) associated to the access token. + * @param scopes the scope(s) associated to the access token, may be {@code null} * @return the {@link Builder} */ - public Builder scopes(Set scopes) { + public Builder scopes(@Nullable Set scopes) { this.scopes = scopes; return this; } /** * Sets the refresh token associated to the access token. - * @param refreshToken the refresh token associated to the access token. + * @param refreshToken the refresh token associated to the access token, may be + * {@code null} * @return the {@link Builder} */ - public Builder refreshToken(String refreshToken) { + public Builder refreshToken(@Nullable String refreshToken) { this.refreshToken = refreshToken; return this; } /** * Sets the additional parameters returned in the response. - * @param additionalParameters the additional parameters returned in the response + * @param additionalParameters the additional parameters returned in the response, + * may be {@code null} * @return the {@link Builder} */ - public Builder additionalParameters(Map additionalParameters) { + public Builder additionalParameters(@Nullable Map additionalParameters) { this.additionalParameters = additionalParameters; return this; } @@ -185,11 +192,14 @@ public final class OAuth2AccessTokenResponse { * @return a {@link OAuth2AccessTokenResponse} */ public OAuth2AccessTokenResponse build() { + Assert.notNull(this.tokenType, "tokenType cannot be null"); Instant issuedAt = getIssuedAt(); Instant expiresAt = getExpiresAt(); + // Convert nullable scopes to non-null for constructor + Set scopesToUse = (this.scopes != null) ? this.scopes : Collections.emptySet(); OAuth2AccessTokenResponse accessTokenResponse = new OAuth2AccessTokenResponse(); accessTokenResponse.accessToken = new OAuth2AccessToken(this.tokenType, this.tokenValue, issuedAt, - expiresAt, this.scopes); + expiresAt, scopesToUse); if (StringUtils.hasText(this.refreshToken)) { accessTokenResponse.refreshToken = new OAuth2RefreshToken(this.refreshToken, issuedAt); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java index b9b48f443f..4e9dcb4704 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java @@ -30,6 +30,8 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -65,11 +67,11 @@ public class OAuth2AuthorizationRequest implements Serializable { private final String clientId; - private final String redirectUri; + private final @Nullable String redirectUri; private final Set scopes; - private final String state; + private final @Nullable String state; private final Map additionalParameters; @@ -80,6 +82,8 @@ public class OAuth2AuthorizationRequest implements Serializable { protected OAuth2AuthorizationRequest(AbstractBuilder builder) { Assert.hasText(builder.authorizationUri, "authorizationUri cannot be empty"); Assert.hasText(builder.clientId, "clientId cannot be empty"); + Assert.notNull(builder.authorizationUri, "authorizationUri cannot be null"); + Assert.notNull(builder.clientId, "clientId cannot be null"); this.authorizationUri = builder.authorizationUri; this.authorizationGrantType = builder.authorizationGrantType; this.responseType = builder.responseType; @@ -89,8 +93,9 @@ public class OAuth2AuthorizationRequest implements Serializable { CollectionUtils.isEmpty(builder.scopes) ? Collections.emptySet() : new LinkedHashSet<>(builder.scopes)); this.state = builder.state; this.additionalParameters = Collections.unmodifiableMap(builder.additionalParameters); - this.authorizationRequestUri = StringUtils.hasText(builder.authorizationRequestUri) - ? builder.authorizationRequestUri : builder.buildAuthorizationRequestUri(); + String builderUri = builder.authorizationRequestUri; + this.authorizationRequestUri = StringUtils.hasText(builderUri) ? builderUri + : builder.buildAuthorizationRequestUri(); this.attributes = Collections.unmodifiableMap(builder.attributes); } @@ -127,10 +132,10 @@ public class OAuth2AuthorizationRequest implements Serializable { } /** - * Returns the uri for the redirection endpoint. - * @return the uri for the redirection endpoint + * Returns the uri for the redirection endpoint, or {@code null} if not present. + * @return the uri for the redirection endpoint, or {@code null} */ - public String getRedirectUri() { + public @Nullable String getRedirectUri() { return this.redirectUri; } @@ -143,10 +148,10 @@ public class OAuth2AuthorizationRequest implements Serializable { } /** - * Returns the state. - * @return the state + * Returns the state, or {@code null} if not present. + * @return the state, or {@code null} */ - public String getState() { + public @Nullable String getState() { return this.state; } @@ -177,7 +182,7 @@ public class OAuth2AuthorizationRequest implements Serializable { * @since 5.2 */ @SuppressWarnings("unchecked") - public T getAttribute(String name) { + public @Nullable T getAttribute(String name) { return (T) this.getAttributes().get(name); } @@ -277,19 +282,19 @@ public class OAuth2AuthorizationRequest implements Serializable { */ protected abstract static class AbstractBuilder> { - private String authorizationUri; + private @Nullable String authorizationUri; private final AuthorizationGrantType authorizationGrantType = AuthorizationGrantType.AUTHORIZATION_CODE; private final OAuth2AuthorizationResponseType responseType = OAuth2AuthorizationResponseType.CODE; - private String clientId; + private @Nullable String clientId; - private String redirectUri; + private @Nullable String redirectUri; - private Set scopes; + private @Nullable Set scopes; - private String state; + private @Nullable String state; private Map additionalParameters = new LinkedHashMap<>(); @@ -298,7 +303,7 @@ public class OAuth2AuthorizationRequest implements Serializable { private Map attributes = new LinkedHashMap<>(); - private String authorizationRequestUri; + private @Nullable String authorizationRequestUri; private Function authorizationRequestUriFunction = (builder) -> builder.build(); @@ -341,20 +346,20 @@ public class OAuth2AuthorizationRequest implements Serializable { /** * Sets the uri for the redirection endpoint. - * @param redirectUri the uri for the redirection endpoint + * @param redirectUri the uri for the redirection endpoint, may be {@code null} * @return the {@link AbstractBuilder} */ - public B redirectUri(String redirectUri) { + public B redirectUri(@Nullable String redirectUri) { this.redirectUri = redirectUri; return getThis(); } /** * Sets the scope(s). - * @param scope the scope(s) + * @param scope the scope(s), may be {@code null} * @return the {@link AbstractBuilder} */ - public B scope(String... scope) { + public B scope(@Nullable String... scope) { if (scope != null && scope.length > 0) { return scopes(new LinkedHashSet<>(Arrays.asList(scope))); } @@ -363,20 +368,20 @@ public class OAuth2AuthorizationRequest implements Serializable { /** * Sets the scope(s). - * @param scopes the scope(s) + * @param scopes the scope(s), may be {@code null} * @return the {@link AbstractBuilder} */ - public B scopes(Set scopes) { + public B scopes(@Nullable Set scopes) { this.scopes = scopes; return getThis(); } /** * Sets the state. - * @param state the state + * @param state the state, may be {@code null} * @return the {@link AbstractBuilder} */ - public B state(String state) { + public B state(@Nullable String state) { this.state = state; return getThis(); } @@ -502,7 +507,9 @@ public class OAuth2AuthorizationRequest implements Serializable { queryParams.set(key, encodeQueryParam(String.valueOf(v))); } }); - UriBuilder uriBuilder = this.uriBuilderFactory.uriString(this.authorizationUri).queryParams(queryParams); + String uri = this.authorizationUri; + Assert.notNull(uri, "authorizationUri cannot be null"); + UriBuilder uriBuilder = this.uriBuilderFactory.uriString(uri).queryParams(queryParams); return this.authorizationRequestUriFunction.apply(uriBuilder).toString(); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationResponse.java index e47fd37e77..f030ab743d 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationResponse.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationResponse.java @@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core.endpoint; import java.io.Serial; import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -39,13 +41,13 @@ public final class OAuth2AuthorizationResponse implements Serializable { @Serial private static final long serialVersionUID = 620L; - private String redirectUri; + private @Nullable String redirectUri; - private String state; + private @Nullable String state; - private String code; + private @Nullable String code; - private OAuth2Error error; + private @Nullable OAuth2Error error; private OAuth2AuthorizationResponse() { } @@ -55,22 +57,24 @@ public final class OAuth2AuthorizationResponse implements Serializable { * @return the uri where the response was redirected to */ public String getRedirectUri() { + Assert.notNull(this.redirectUri, "redirectUri cannot be null"); return this.redirectUri; } /** - * Returns the state. - * @return the state + * Returns the state, or {@code null} if not present. + * @return the state, or {@code null} */ - public String getState() { + public @Nullable String getState() { return this.state; } /** - * Returns the authorization code. - * @return the authorization code + * Returns the authorization code, or {@code null} if the response is an error + * response. + * @return the authorization code, or {@code null} */ - public String getCode() { + public @Nullable String getCode() { return this.code; } @@ -80,7 +84,7 @@ public final class OAuth2AuthorizationResponse implements Serializable { * @return the {@link OAuth2Error} if the Authorization Request failed, otherwise * {@code null} */ - public OAuth2Error getError() { + public @Nullable OAuth2Error getError() { return this.error; } @@ -127,17 +131,17 @@ public final class OAuth2AuthorizationResponse implements Serializable { */ public static final class Builder { - private String redirectUri; + private @Nullable String redirectUri; - private String state; + private @Nullable String state; - private String code; + private @Nullable String code; - private String errorCode; + private @Nullable String errorCode; - private String errorDescription; + private @Nullable String errorDescription; - private String errorUri; + private @Nullable String errorUri; private Builder() { } @@ -218,6 +222,7 @@ public final class OAuth2AuthorizationResponse implements Serializable { authorizationResponse.code = this.code; } else { + Assert.hasText(this.errorCode, "errorCode cannot be empty when code is not present"); authorizationResponse.error = new OAuth2Error(this.errorCode, this.errorDescription, this.errorUri); } return authorizationResponse; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java index 18ca78d770..da2ce84110 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2DeviceAuthorizationResponse.java @@ -21,6 +21,8 @@ import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.OAuth2DeviceCode; import org.springframework.security.oauth2.core.OAuth2UserCode; import org.springframework.util.Assert; @@ -38,17 +40,17 @@ import org.springframework.util.CollectionUtils; */ public final class OAuth2DeviceAuthorizationResponse { - private OAuth2DeviceCode deviceCode; + private @Nullable OAuth2DeviceCode deviceCode; - private OAuth2UserCode userCode; + private @Nullable OAuth2UserCode userCode; - private String verificationUri; + private @Nullable String verificationUri; - private String verificationUriComplete; + private @Nullable String verificationUriComplete; private long interval; - private Map additionalParameters; + private @Nullable Map additionalParameters; private OAuth2DeviceAuthorizationResponse() { } @@ -58,6 +60,7 @@ public final class OAuth2DeviceAuthorizationResponse { * @return the {@link OAuth2DeviceCode} */ public OAuth2DeviceCode getDeviceCode() { + Assert.notNull(this.deviceCode, "deviceCode cannot be null"); return this.deviceCode; } @@ -66,6 +69,7 @@ public final class OAuth2DeviceAuthorizationResponse { * @return the {@link OAuth2UserCode} */ public OAuth2UserCode getUserCode() { + Assert.notNull(this.userCode, "userCode cannot be null"); return this.userCode; } @@ -74,14 +78,16 @@ public final class OAuth2DeviceAuthorizationResponse { * @return the end-user verification URI */ public String getVerificationUri() { + Assert.notNull(this.verificationUri, "verificationUri cannot be null"); return this.verificationUri; } /** - * Returns the end-user verification URI that includes the user code. - * @return the end-user verification URI that includes the user code + * Returns the end-user verification URI that includes the user code, or {@code null} + * if not present. + * @return the end-user verification URI that includes the user code, or {@code null} */ - public String getVerificationUriComplete() { + public @Nullable String getVerificationUriComplete() { return this.verificationUriComplete; } @@ -100,6 +106,7 @@ public final class OAuth2DeviceAuthorizationResponse { * empty. */ public Map getAdditionalParameters() { + Assert.notNull(this.additionalParameters, "additionalParameters cannot be null"); return this.additionalParameters; } @@ -138,15 +145,15 @@ public final class OAuth2DeviceAuthorizationResponse { private final String userCode; - private String verificationUri; + private @Nullable String verificationUri; - private String verificationUriComplete; + private @Nullable String verificationUriComplete; private long expiresIn; private long interval; - private Map additionalParameters; + private @Nullable Map additionalParameters; private Builder(OAuth2DeviceCode deviceCode, OAuth2UserCode userCode) { this.deviceCode = deviceCode.getTokenValue(); @@ -172,10 +179,10 @@ public final class OAuth2DeviceAuthorizationResponse { /** * Sets the end-user verification URI that includes the user code. * @param verificationUriComplete the end-user verification URI that includes the - * user code + * user code, may be {@code null} * @return the {@link Builder} */ - public Builder verificationUriComplete(String verificationUriComplete) { + public Builder verificationUriComplete(@Nullable String verificationUriComplete) { this.verificationUriComplete = verificationUriComplete; return this; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java index 595dadafdb..6649cabdfb 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/package-info.java @@ -18,4 +18,7 @@ * Support classes that model the OAuth 2.0 Request and Response messages from the * Authorization Endpoint and Token Endpoint. */ +@NullMarked package org.springframework.security.oauth2.core.endpoint; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/HttpMessageConverters.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/HttpMessageConverters.java index fca64a7667..1e2d4a9252 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/HttpMessageConverters.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/HttpMessageConverters.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.core.http.converter; +import org.jspecify.annotations.Nullable; + import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; @@ -54,7 +56,7 @@ final class HttpMessageConverters { } @SuppressWarnings("removal") - static GenericHttpMessageConverter getJsonMessageConverter() { + static @Nullable GenericHttpMessageConverter getJsonMessageConverter() { if (jacksonPresent) { return new GenericHttpMessageConverterAdapter<>(new JacksonJsonHttpMessageConverter()); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverter.java index f431dea5a3..ae8d24e5b6 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2AccessTokenResponseHttpMessageConverter.java @@ -52,8 +52,7 @@ public class OAuth2AccessTokenResponseHttpMessageConverter private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() { }; - private final GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters - .getJsonMessageConverter(); + private final GenericHttpMessageConverter jsonMessageConverter; private Converter, OAuth2AccessTokenResponse> accessTokenResponseConverter = new DefaultMapOAuth2AccessTokenResponseConverter(); @@ -61,6 +60,9 @@ public class OAuth2AccessTokenResponseHttpMessageConverter public OAuth2AccessTokenResponseHttpMessageConverter() { super(DEFAULT_CHARSET, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); + GenericHttpMessageConverter converter = HttpMessageConverters.getJsonMessageConverter(); + Assert.notNull(converter, "Unable to locate a supported JSON message converter"); + this.jsonMessageConverter = converter; } @Override diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java index 7be1205b7f..b2eeb82906 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2DeviceAuthorizationResponseHttpMessageConverter.java @@ -25,6 +25,8 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpInputMessage; @@ -56,13 +58,18 @@ public class OAuth2DeviceAuthorizationResponseHttpMessageConverter private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() { }; - private final GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters - .getJsonMessageConverter(); + private final GenericHttpMessageConverter jsonMessageConverter; private Converter, OAuth2DeviceAuthorizationResponse> deviceAuthorizationResponseConverter = new DefaultMapOAuth2DeviceAuthorizationResponseConverter(); private Converter> deviceAuthorizationResponseParametersConverter = new DefaultOAuth2DeviceAuthorizationResponseMapConverter(); + public OAuth2DeviceAuthorizationResponseHttpMessageConverter() { + GenericHttpMessageConverter converter = HttpMessageConverters.getJsonMessageConverter(); + Assert.notNull(converter, "Unable to locate a supported JSON message converter"); + this.jsonMessageConverter = converter; + } + @Override protected boolean supports(Class clazz) { return OAuth2DeviceAuthorizationResponse.class.isAssignableFrom(clazz); @@ -139,8 +146,18 @@ public class OAuth2DeviceAuthorizationResponseHttpMessageConverter @Override public OAuth2DeviceAuthorizationResponse convert(Map parameters) { String deviceCode = getParameterValue(parameters, OAuth2ParameterNames.DEVICE_CODE); + if (deviceCode == null) { + throw new IllegalArgumentException("Missing required parameter: " + OAuth2ParameterNames.DEVICE_CODE); + } String userCode = getParameterValue(parameters, OAuth2ParameterNames.USER_CODE); + if (userCode == null) { + throw new IllegalArgumentException("Missing required parameter: " + OAuth2ParameterNames.USER_CODE); + } String verificationUri = getParameterValue(parameters, OAuth2ParameterNames.VERIFICATION_URI); + if (verificationUri == null) { + throw new IllegalArgumentException( + "Missing required parameter: " + OAuth2ParameterNames.VERIFICATION_URI); + } String verificationUriComplete = getParameterValue(parameters, OAuth2ParameterNames.VERIFICATION_URI_COMPLETE); long expiresIn = getParameterValue(parameters, OAuth2ParameterNames.EXPIRES_IN, 0L); @@ -162,7 +179,7 @@ public class OAuth2DeviceAuthorizationResponseHttpMessageConverter // @formatter:on } - private static String getParameterValue(Map parameters, String parameterName) { + private static @Nullable String getParameterValue(Map parameters, String parameterName) { Object obj = parameters.get(parameterName); return (obj != null) ? obj.toString() : null; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverter.java index 70e0556bf3..eb364f63e4 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverter.java @@ -52,8 +52,7 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() { }; - private final GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters - .getJsonMessageConverter(); + private final GenericHttpMessageConverter jsonMessageConverter; protected Converter, OAuth2Error> errorConverter = new OAuth2ErrorConverter(); @@ -61,6 +60,9 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte public OAuth2ErrorHttpMessageConverter() { super(DEFAULT_CHARSET, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); + GenericHttpMessageConverter converter = HttpMessageConverters.getJsonMessageConverter(); + Assert.notNull(converter, "Unable to locate a supported JSON message converter"); + this.jsonMessageConverter = converter; } @Override @@ -133,6 +135,7 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverte @Override public OAuth2Error convert(Map parameters) { String errorCode = parameters.get(OAuth2ParameterNames.ERROR); + Assert.hasText(errorCode, "errorCode cannot be empty"); String errorDescription = parameters.get(OAuth2ParameterNames.ERROR_DESCRIPTION); String errorUri = parameters.get(OAuth2ParameterNames.ERROR_URI); return new OAuth2Error(errorCode, errorDescription, errorUri); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/package-info.java new file mode 100644 index 0000000000..a3f7b61aa2 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/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. + */ + +/** + * HTTP message converters for OAuth 2.0 and OpenID Connect protocol messages. + */ +@NullMarked +package org.springframework.security.oauth2.core.http.converter; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/package-info.java new file mode 100644 index 0000000000..cf38bd61a4 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/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 that provide HTTP message conversion for OAuth 2.0 and OpenID Connect. + */ +@NullMarked +package org.springframework.security.oauth2.core.http; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/AddressStandardClaim.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/AddressStandardClaim.java index 6329e5f160..a4ba432996 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/AddressStandardClaim.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/AddressStandardClaim.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.core.oidc; +import org.jspecify.annotations.Nullable; + /** * The Address Claim represents a physical mailing address defined by the OpenID Connect * Core 1.0 specification that can be returned either in the UserInfo Response or the ID @@ -34,40 +36,43 @@ package org.springframework.security.oauth2.core.oidc; public interface AddressStandardClaim { /** - * Returns the full mailing address, formatted for display. - * @return the full mailing address + * Returns the full mailing address, formatted for display, or {@code null} if it does + * not exist. + * @return the full mailing address, or {@code null} if it does not exist */ - String getFormatted(); + @Nullable String getFormatted(); /** * Returns the full street address, which may include house number, street name, P.O. - * Box, etc. - * @return the full street address + * Box, etc., or {@code null} if it does not exist. + * @return the full street address, or {@code null} if it does not exist */ - String getStreetAddress(); + @Nullable String getStreetAddress(); /** - * Returns the city or locality. - * @return the city or locality + * Returns the city or locality, or {@code null} if it does not exist. + * @return the city or locality, or {@code null} if it does not exist */ - String getLocality(); + @Nullable String getLocality(); /** - * Returns the state, province, prefecture, or region. - * @return the state, province, prefecture, or region + * Returns the state, province, prefecture, or region, or {@code null} if it does not + * exist. + * @return the state, province, prefecture, or region, or {@code null} if it does not + * exist */ - String getRegion(); + @Nullable String getRegion(); /** - * Returns the zip code or postal code. - * @return the zip code or postal code + * Returns the zip code or postal code, or {@code null} if it does not exist. + * @return the zip code or postal code, or {@code null} if it does not exist */ - String getPostalCode(); + @Nullable String getPostalCode(); /** - * Returns the country. - * @return the country + * Returns the country, or {@code null} if it does not exist. + * @return the country, or {@code null} if it does not exist */ - String getCountry(); + @Nullable String getCountry(); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/DefaultAddressStandardClaim.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/DefaultAddressStandardClaim.java index 8cecca5c15..28360753bd 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/DefaultAddressStandardClaim.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/DefaultAddressStandardClaim.java @@ -18,6 +18,8 @@ package org.springframework.security.oauth2.core.oidc; import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * The default implementation of an {@link AddressStandardClaim Address Claim}. * @@ -27,48 +29,48 @@ import java.util.Map; */ public final class DefaultAddressStandardClaim implements AddressStandardClaim { - private String formatted; + private @Nullable String formatted; - private String streetAddress; + private @Nullable String streetAddress; - private String locality; + private @Nullable String locality; - private String region; + private @Nullable String region; - private String postalCode; + private @Nullable String postalCode; - private String country; + private @Nullable String country; private DefaultAddressStandardClaim() { } @Override - public String getFormatted() { + public @Nullable String getFormatted() { return this.formatted; } @Override - public String getStreetAddress() { + public @Nullable String getStreetAddress() { return this.streetAddress; } @Override - public String getLocality() { + public @Nullable String getLocality() { return this.locality; } @Override - public String getRegion() { + public @Nullable String getRegion() { return this.region; } @Override - public String getPostalCode() { + public @Nullable String getPostalCode() { return this.postalCode; } @Override - public String getCountry() { + public @Nullable String getCountry() { return this.country; } @@ -131,17 +133,17 @@ public final class DefaultAddressStandardClaim implements AddressStandardClaim { private static final String COUNTRY_FIELD_NAME = "country"; - private String formatted; + private @Nullable String formatted; - private String streetAddress; + private @Nullable String streetAddress; - private String locality; + private @Nullable String locality; - private String region; + private @Nullable String region; - private String postalCode; + private @Nullable String postalCode; - private String country; + private @Nullable String country; /** * Default constructor. @@ -165,10 +167,10 @@ public final class DefaultAddressStandardClaim implements AddressStandardClaim { /** * Sets the full mailing address, formatted for display. - * @param formatted the full mailing address + * @param formatted the full mailing address, may be {@code null} * @return the {@link Builder} */ - public Builder formatted(String formatted) { + public Builder formatted(@Nullable String formatted) { this.formatted = formatted; return this; } @@ -176,50 +178,50 @@ public final class DefaultAddressStandardClaim implements AddressStandardClaim { /** * Sets the full street address, which may include house number, street name, P.O. * Box, etc. - * @param streetAddress the full street address + * @param streetAddress the full street address, may be {@code null} * @return the {@link Builder} */ - public Builder streetAddress(String streetAddress) { + public Builder streetAddress(@Nullable String streetAddress) { this.streetAddress = streetAddress; return this; } /** * Sets the city or locality. - * @param locality the city or locality + * @param locality the city or locality, may be {@code null} * @return the {@link Builder} */ - public Builder locality(String locality) { + public Builder locality(@Nullable String locality) { this.locality = locality; return this; } /** * Sets the state, province, prefecture, or region. - * @param region the state, province, prefecture, or region + * @param region the state, province, prefecture, or region, may be {@code null} * @return the {@link Builder} */ - public Builder region(String region) { + public Builder region(@Nullable String region) { this.region = region; return this; } /** * Sets the zip code or postal code. - * @param postalCode the zip code or postal code + * @param postalCode the zip code or postal code, may be {@code null} * @return the {@link Builder} */ - public Builder postalCode(String postalCode) { + public Builder postalCode(@Nullable String postalCode) { this.postalCode = postalCode; return this; } /** * Sets the country. - * @param country the country + * @param country the country, may be {@code null} * @return the {@link Builder} */ - public Builder country(String country) { + public Builder country(@Nullable String country) { this.country = country; return this; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/IdTokenClaimAccessor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/IdTokenClaimAccessor.java index a75a13b4f1..572ef75c09 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/IdTokenClaimAccessor.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/IdTokenClaimAccessor.java @@ -20,6 +20,8 @@ import java.net.URL; import java.time.Instant; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.ClaimAccessor; /** @@ -43,101 +45,117 @@ import org.springframework.security.oauth2.core.ClaimAccessor; public interface IdTokenClaimAccessor extends StandardClaimAccessor { /** - * Returns the Issuer identifier {@code (iss)}. - * @return the Issuer identifier + * Returns the Issuer identifier {@code (iss)}, or {@code null} if it does not exist. + * @return the Issuer identifier, or {@code null} if it does not exist */ - default URL getIssuer() { + default @Nullable URL getIssuer() { return this.getClaimAsURL(IdTokenClaimNames.ISS); } /** - * Returns the Subject identifier {@code (sub)}. - * @return the Subject identifier + * Returns the Subject identifier {@code (sub)}, or {@code null} if it does not exist. + * @return the Subject identifier, or {@code null} if it does not exist */ @Override - default String getSubject() { + default @Nullable String getSubject() { return this.getClaimAsString(IdTokenClaimNames.SUB); } /** - * Returns the Audience(s) {@code (aud)} that this ID Token is intended for. - * @return the Audience(s) that this ID Token is intended for + * Returns the Audience(s) {@code (aud)} that this ID Token is intended for, or + * {@code null} if it does not exist. + * @return the Audience(s) that this ID Token is intended for, or {@code null} if it + * does not exist */ - default List getAudience() { + default @Nullable List getAudience() { return this.getClaimAsStringList(IdTokenClaimNames.AUD); } /** * Returns the Expiration time {@code (exp)} on or after which the ID Token MUST NOT - * be accepted. - * @return the Expiration time on or after which the ID Token MUST NOT be accepted + * be accepted, or {@code null} if it does not exist. + * @return the Expiration time on or after which the ID Token MUST NOT be accepted, or + * {@code null} if it does not exist */ - default Instant getExpiresAt() { + default @Nullable Instant getExpiresAt() { return this.getClaimAsInstant(IdTokenClaimNames.EXP); } /** - * Returns the time at which the ID Token was issued {@code (iat)}. - * @return the time at which the ID Token was issued + * Returns the time at which the ID Token was issued {@code (iat)}, or {@code null} if + * it does not exist. + * @return the time at which the ID Token was issued, or {@code null} if it does not + * exist */ - default Instant getIssuedAt() { + default @Nullable Instant getIssuedAt() { return this.getClaimAsInstant(IdTokenClaimNames.IAT); } /** - * Returns the time when the End-User authentication occurred {@code (auth_time)}. - * @return the time when the End-User authentication occurred + * Returns the time when the End-User authentication occurred {@code (auth_time)}, or + * {@code null} if it does not exist. + * @return the time when the End-User authentication occurred, or {@code null} if it + * does not exist */ - default Instant getAuthenticatedAt() { + default @Nullable Instant getAuthenticatedAt() { return this.getClaimAsInstant(IdTokenClaimNames.AUTH_TIME); } /** * Returns a {@code String} value {@code (nonce)} used to associate a Client session - * with an ID Token, and to mitigate replay attacks. - * @return the nonce used to associate a Client session with an ID Token + * with an ID Token, and to mitigate replay attacks, or {@code null} if it does not + * exist. + * @return the nonce used to associate a Client session with an ID Token, or + * {@code null} if it does not exist */ - default String getNonce() { + default @Nullable String getNonce() { return this.getClaimAsString(IdTokenClaimNames.NONCE); } /** - * Returns the Authentication Context Class Reference {@code (acr)}. - * @return the Authentication Context Class Reference + * Returns the Authentication Context Class Reference {@code (acr)}, or {@code null} + * if it does not exist. + * @return the Authentication Context Class Reference, or {@code null} if it does not + * exist */ - default String getAuthenticationContextClass() { + default @Nullable String getAuthenticationContextClass() { return this.getClaimAsString(IdTokenClaimNames.ACR); } /** - * Returns the Authentication Methods References {@code (amr)}. - * @return the Authentication Methods References + * Returns the Authentication Methods References {@code (amr)}, or {@code null} if it + * does not exist. + * @return the Authentication Methods References, or {@code null} if it does not exist */ - default List getAuthenticationMethods() { + default @Nullable List getAuthenticationMethods() { return this.getClaimAsStringList(IdTokenClaimNames.AMR); } /** - * Returns the Authorized party {@code (azp)} to which the ID Token was issued. - * @return the Authorized party to which the ID Token was issued + * Returns the Authorized party {@code (azp)} to which the ID Token was issued, or + * {@code null} if it does not exist. + * @return the Authorized party to which the ID Token was issued, or {@code null} if + * it does not exist */ - default String getAuthorizedParty() { + default @Nullable String getAuthorizedParty() { return this.getClaimAsString(IdTokenClaimNames.AZP); } /** - * Returns the Access Token hash value {@code (at_hash)}. - * @return the Access Token hash value + * Returns the Access Token hash value {@code (at_hash)}, or {@code null} if it does + * not exist. + * @return the Access Token hash value, or {@code null} if it does not exist */ - default String getAccessTokenHash() { + default @Nullable String getAccessTokenHash() { return this.getClaimAsString(IdTokenClaimNames.AT_HASH); } /** - * Returns the Authorization Code hash value {@code (c_hash)}. - * @return the Authorization Code hash value + * Returns the Authorization Code hash value {@code (c_hash)}, or {@code null} if it + * does not exist. + * @return the Authorization Code hash value, or {@code null} if it does not exist */ - default String getAuthorizationCodeHash() { + default @Nullable String getAuthorizationCodeHash() { return this.getClaimAsString(IdTokenClaimNames.C_HASH); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcIdToken.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcIdToken.java index bff451f999..fd50c63870 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcIdToken.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcIdToken.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.security.oauth2.core.AbstractOAuth2Token; import org.springframework.util.Assert; @@ -57,12 +59,14 @@ public class OidcIdToken extends AbstractOAuth2Token implements IdTokenClaimAcce /** * Constructs a {@code OidcIdToken} using the provided parameters. * @param tokenValue the ID Token value - * @param issuedAt the time at which the ID Token was issued {@code (iat)} + * @param issuedAt the time at which the ID Token was issued {@code (iat)}, may be + * {@code null} * @param expiresAt the expiration time {@code (exp)} on or after which the ID Token - * MUST NOT be accepted + * MUST NOT be accepted, may be {@code null} * @param claims the claims about the authentication of the End-User */ - public OidcIdToken(String tokenValue, Instant issuedAt, Instant expiresAt, Map claims) { + public OidcIdToken(String tokenValue, @Nullable Instant issuedAt, @Nullable Instant expiresAt, + Map claims) { super(tokenValue, issuedAt, expiresAt); Assert.notEmpty(claims, "claims cannot be empty"); this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims)); @@ -246,12 +250,14 @@ public class OidcIdToken extends AbstractOAuth2Token implements IdTokenClaimAcce * @return The constructed {@link OidcIdToken} */ public OidcIdToken build() { - Instant iat = toInstant(this.claims.get(IdTokenClaimNames.IAT)); - Instant exp = toInstant(this.claims.get(IdTokenClaimNames.EXP)); + Object iatObj = this.claims.get(IdTokenClaimNames.IAT); + Object expObj = this.claims.get(IdTokenClaimNames.EXP); + Instant iat = toInstant(iatObj); + Instant exp = toInstant(expObj); return new OidcIdToken(this.tokenValue, iat, exp, this.claims); } - 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-core/src/main/java/org/springframework/security/oauth2/core/oidc/StandardClaimAccessor.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/StandardClaimAccessor.java index 08163d7ca4..d27638b3a7 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/StandardClaimAccessor.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/StandardClaimAccessor.java @@ -19,6 +19,8 @@ package org.springframework.security.oauth2.core.oidc; import java.time.Instant; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.ClaimAccessor; import org.springframework.util.CollectionUtils; @@ -41,152 +43,166 @@ import org.springframework.util.CollectionUtils; public interface StandardClaimAccessor extends ClaimAccessor { /** - * Returns the Subject identifier {@code (sub)}. - * @return the Subject identifier + * Returns the Subject identifier {@code (sub)}, or {@code null} if it does not exist. + * @return the Subject identifier, or {@code null} if it does not exist */ - default String getSubject() { + default @Nullable String getSubject() { return this.getClaimAsString(StandardClaimNames.SUB); } /** - * Returns the user's full name {@code (name)} in displayable form. - * @return the user's full name + * Returns the user's full name {@code (name)} in displayable form, or {@code null} if + * it does not exist. + * @return the user's full name, or {@code null} if it does not exist */ - default String getFullName() { + default @Nullable String getFullName() { return this.getClaimAsString(StandardClaimNames.NAME); } /** - * Returns the user's given name(s) or first name(s) {@code (given_name)}. - * @return the user's given name(s) + * Returns the user's given name(s) or first name(s) {@code (given_name)}, or + * {@code null} if it does not exist. + * @return the user's given name(s), or {@code null} if it does not exist */ - default String getGivenName() { + default @Nullable String getGivenName() { return this.getClaimAsString(StandardClaimNames.GIVEN_NAME); } /** - * Returns the user's surname(s) or last name(s) {@code (family_name)}. - * @return the user's family names(s) + * Returns the user's surname(s) or last name(s) {@code (family_name)}, or + * {@code null} if it does not exist. + * @return the user's family names(s), or {@code null} if it does not exist */ - default String getFamilyName() { + default @Nullable String getFamilyName() { return this.getClaimAsString(StandardClaimNames.FAMILY_NAME); } /** - * Returns the user's middle name(s) {@code (middle_name)}. - * @return the user's middle name(s) + * Returns the user's middle name(s) {@code (middle_name)}, or {@code null} if it does + * not exist. + * @return the user's middle name(s), or {@code null} if it does not exist */ - default String getMiddleName() { + default @Nullable String getMiddleName() { return this.getClaimAsString(StandardClaimNames.MIDDLE_NAME); } /** * Returns the user's nick name {@code (nickname)} that may or may not be the same as - * the {@code (given_name)}. - * @return the user's nick name + * the {@code (given_name)}, or {@code null} if it does not exist. + * @return the user's nick name, or {@code null} if it does not exist */ - default String getNickName() { + default @Nullable String getNickName() { return this.getClaimAsString(StandardClaimNames.NICKNAME); } /** * Returns the preferred username {@code (preferred_username)} that the user wishes to - * be referred to. - * @return the user's preferred user name + * be referred to, or {@code null} if it does not exist. + * @return the user's preferred user name, or {@code null} if it does not exist */ - default String getPreferredUsername() { + default @Nullable String getPreferredUsername() { return this.getClaimAsString(StandardClaimNames.PREFERRED_USERNAME); } /** - * Returns the URL of the user's profile page {@code (profile)}. - * @return the URL of the user's profile page + * Returns the URL of the user's profile page {@code (profile)}, or {@code null} if it + * does not exist. + * @return the URL of the user's profile page, or {@code null} if it does not exist */ - default String getProfile() { + default @Nullable String getProfile() { return this.getClaimAsString(StandardClaimNames.PROFILE); } /** - * Returns the URL of the user's profile picture {@code (picture)}. - * @return the URL of the user's profile picture + * Returns the URL of the user's profile picture {@code (picture)}, or {@code null} if + * it does not exist. + * @return the URL of the user's profile picture, or {@code null} if it does not exist */ - default String getPicture() { + default @Nullable String getPicture() { return this.getClaimAsString(StandardClaimNames.PICTURE); } /** - * Returns the URL of the user's web page or blog {@code (website)}. - * @return the URL of the user's web page or blog + * Returns the URL of the user's web page or blog {@code (website)}, or {@code null} + * if it does not exist. + * @return the URL of the user's web page or blog, or {@code null} if it does not + * exist */ - default String getWebsite() { + default @Nullable String getWebsite() { return this.getClaimAsString(StandardClaimNames.WEBSITE); } /** - * Returns the user's preferred e-mail address {@code (email)}. - * @return the user's preferred e-mail address + * Returns the user's preferred e-mail address {@code (email)}, or {@code null} if it + * does not exist. + * @return the user's preferred e-mail address, or {@code null} if it does not exist */ - default String getEmail() { + default @Nullable String getEmail() { return this.getClaimAsString(StandardClaimNames.EMAIL); } /** * Returns {@code true} if the user's e-mail address has been verified - * {@code (email_verified)}, otherwise {@code false}. + * {@code (email_verified)}, otherwise {@code false}, or {@code null} if it does not + * exist. * @return {@code true} if the user's e-mail address has been verified, otherwise - * {@code false} + * {@code false}, or {@code null} if it does not exist */ - default Boolean getEmailVerified() { + default @Nullable Boolean getEmailVerified() { return this.getClaimAsBoolean(StandardClaimNames.EMAIL_VERIFIED); } /** - * Returns the user's gender {@code (gender)}. - * @return the user's gender + * Returns the user's gender {@code (gender)}, or {@code null} if it does not exist. + * @return the user's gender, or {@code null} if it does not exist */ - default String getGender() { + default @Nullable String getGender() { return this.getClaimAsString(StandardClaimNames.GENDER); } /** - * Returns the user's birth date {@code (birthdate)}. - * @return the user's birth date + * Returns the user's birth date {@code (birthdate)}, or {@code null} if it does not + * exist. + * @return the user's birth date, or {@code null} if it does not exist */ - default String getBirthdate() { + default @Nullable String getBirthdate() { return this.getClaimAsString(StandardClaimNames.BIRTHDATE); } /** - * Returns the user's time zone {@code (zoneinfo)}. - * @return the user's time zone + * Returns the user's time zone {@code (zoneinfo)}, or {@code null} if it does not + * exist. + * @return the user's time zone, or {@code null} if it does not exist */ - default String getZoneInfo() { + default @Nullable String getZoneInfo() { return this.getClaimAsString(StandardClaimNames.ZONEINFO); } /** - * Returns the user's locale {@code (locale)}. - * @return the user's locale + * Returns the user's locale {@code (locale)}, or {@code null} if it does not exist. + * @return the user's locale, or {@code null} if it does not exist */ - default String getLocale() { + default @Nullable String getLocale() { return this.getClaimAsString(StandardClaimNames.LOCALE); } /** - * Returns the user's preferred phone number {@code (phone_number)}. - * @return the user's preferred phone number + * Returns the user's preferred phone number {@code (phone_number)}, or {@code null} + * if it does not exist. + * @return the user's preferred phone number, or {@code null} if it does not exist */ - default String getPhoneNumber() { + default @Nullable String getPhoneNumber() { return this.getClaimAsString(StandardClaimNames.PHONE_NUMBER); } /** * Returns {@code true} if the user's phone number has been verified - * {@code (phone_number_verified)}, otherwise {@code false}. + * {@code (phone_number_verified)}, otherwise {@code false}, or {@code null} if it + * does not exist. * @return {@code true} if the user's phone number has been verified, otherwise - * {@code false} + * {@code false}, or {@code null} if it does not exist */ - default Boolean getPhoneNumberVerified() { + default @Nullable Boolean getPhoneNumberVerified() { return this.getClaimAsBoolean(StandardClaimNames.PHONE_NUMBER_VERIFIED); } @@ -201,10 +217,12 @@ public interface StandardClaimAccessor extends ClaimAccessor { } /** - * Returns the time the user's information was last updated {@code (updated_at)}. - * @return the time the user's information was last updated + * Returns the time the user's information was last updated {@code (updated_at)}, or + * {@code null} if it does not exist. + * @return the time the user's information was last updated, or {@code null} if it + * does not exist */ - default Instant getUpdatedAt() { + default @Nullable Instant getUpdatedAt() { return this.getClaimAsInstant(StandardClaimNames.UPDATED_AT); } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/endpoint/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/endpoint/package-info.java index 7b73d843a7..3aa82bf1c3 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/endpoint/package-info.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/endpoint/package-info.java @@ -18,4 +18,7 @@ * Support classes that model the OpenID Connect Core 1.0 Request and Response messages * from the Authorization Endpoint and Token Endpoint. */ +@NullMarked package org.springframework.security.oauth2.core.oidc.endpoint; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/package-info.java index 4b48e438b4..8cb1d1892f 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/package-info.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/package-info.java @@ -17,4 +17,7 @@ /** * Core classes and interfaces providing support for OpenID Connect Core 1.0. */ +@NullMarked package org.springframework.security.oauth2.core.oidc; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUser.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUser.java index 3b99e3e829..6e1284fdbf 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUser.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUser.java @@ -20,6 +20,8 @@ import java.io.Serial; import java.util.Collection; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; @@ -48,52 +50,52 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser { private final OidcIdToken idToken; - private final OidcUserInfo userInfo; + private final @Nullable OidcUserInfo userInfo; /** * Constructs a {@code DefaultOidcUser} using the provided parameters. - * @param authorities the authorities granted to the user + * @param authorities the authorities granted to the user, may be {@code null} * @param idToken the {@link OidcIdToken ID Token} containing claims about the user */ - public DefaultOidcUser(Collection authorities, OidcIdToken idToken) { + public DefaultOidcUser(@Nullable Collection authorities, OidcIdToken idToken) { this(authorities, idToken, IdTokenClaimNames.SUB); } /** * Constructs a {@code DefaultOidcUser} using the provided parameters. - * @param authorities the authorities granted to the user + * @param authorities the authorities granted to the user, may be {@code null} * @param idToken the {@link OidcIdToken ID Token} containing claims about the user * @param nameAttributeKey the key used to access the user's "name" from * {@link #getAttributes()} */ - public DefaultOidcUser(Collection authorities, OidcIdToken idToken, + public DefaultOidcUser(@Nullable Collection authorities, OidcIdToken idToken, String nameAttributeKey) { this(authorities, idToken, null, nameAttributeKey); } /** * Constructs a {@code DefaultOidcUser} using the provided parameters. - * @param authorities the authorities granted to the user + * @param authorities the authorities granted to the user, may be {@code null} * @param idToken the {@link OidcIdToken ID Token} containing claims about the user * @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user, * may be {@code null} */ - public DefaultOidcUser(Collection authorities, OidcIdToken idToken, - OidcUserInfo userInfo) { + public DefaultOidcUser(@Nullable Collection authorities, OidcIdToken idToken, + @Nullable OidcUserInfo userInfo) { this(authorities, idToken, userInfo, IdTokenClaimNames.SUB); } /** * Constructs a {@code DefaultOidcUser} using the provided parameters. - * @param authorities the authorities granted to the user + * @param authorities the authorities granted to the user, may be {@code null} * @param idToken the {@link OidcIdToken ID Token} containing claims about the user * @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user, * may be {@code null} * @param nameAttributeKey the key used to access the user's "name" from * {@link #getAttributes()} */ - public DefaultOidcUser(Collection authorities, OidcIdToken idToken, - OidcUserInfo userInfo, String nameAttributeKey) { + public DefaultOidcUser(@Nullable Collection authorities, OidcIdToken idToken, + @Nullable OidcUserInfo userInfo, String nameAttributeKey) { super(authorities, OidcUserAuthority.collectClaims(idToken, userInfo), nameAttributeKey); this.idToken = idToken; this.userInfo = userInfo; @@ -110,7 +112,7 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser { } @Override - public OidcUserInfo getUserInfo() { + public @Nullable OidcUserInfo getUserInfo() { return this.userInfo; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUser.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUser.java index b795215a40..01e87e22cb 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUser.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUser.java @@ -18,6 +18,8 @@ package org.springframework.security.oauth2.core.oidc.user; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.oidc.IdTokenClaimAccessor; @@ -65,10 +67,11 @@ public interface OidcUser extends OAuth2User, IdTokenClaimAccessor { Map getClaims(); /** - * Returns the {@link OidcUserInfo UserInfo} containing claims about the user. - * @return the {@link OidcUserInfo} containing claims about the user. + * Returns the {@link OidcUserInfo UserInfo} containing claims about the user, or + * {@code null} if not present. + * @return the {@link OidcUserInfo} containing claims about the user, or {@code null} */ - OidcUserInfo getUserInfo(); + @Nullable OidcUserInfo getUserInfo(); /** * Returns the {@link OidcIdToken ID Token} containing claims about the user. diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthority.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthority.java index 5e0f4fa0b2..3b6acab6e7 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthority.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthority.java @@ -19,8 +19,10 @@ package org.springframework.security.oauth2.core.oidc.user; import java.io.Serial; import java.util.HashMap; import java.util.Map; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; @@ -42,7 +44,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority { private final OidcIdToken idToken; - private final OidcUserInfo userInfo; + private final @Nullable OidcUserInfo userInfo; /** * Constructs a {@code OidcUserAuthority} using the provided parameters. @@ -59,7 +61,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority { * @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user, * may be {@code null} */ - public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo) { + public OidcUserAuthority(OidcIdToken idToken, @Nullable OidcUserInfo userInfo) { this("OIDC_USER", idToken, userInfo); } @@ -70,10 +72,11 @@ public class OidcUserAuthority extends OAuth2UserAuthority { * @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user, * may be {@code null} * @param userNameAttributeName the attribute name used to access the user's name from - * the attributes + * the attributes, may be {@code null} * @since 6.4 */ - public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo, @Nullable String userNameAttributeName) { + public OidcUserAuthority(OidcIdToken idToken, @Nullable OidcUserInfo userInfo, + @Nullable String userNameAttributeName) { this("OIDC_USER", idToken, userInfo, userNameAttributeName); } @@ -84,7 +87,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority { * @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user, * may be {@code null} */ - public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo) { + public OidcUserAuthority(String authority, OidcIdToken idToken, @Nullable OidcUserInfo userInfo) { this(authority, idToken, userInfo, IdTokenClaimNames.SUB); } @@ -95,10 +98,10 @@ public class OidcUserAuthority extends OAuth2UserAuthority { * @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user, * may be {@code null} * @param userNameAttributeName the attribute name used to access the user's name from - * the attributes + * the attributes, may be {@code null} * @since 6.4 */ - public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo, + public OidcUserAuthority(String authority, OidcIdToken idToken, @Nullable OidcUserInfo userInfo, @Nullable String userNameAttributeName) { super(authority, collectClaims(idToken, userInfo), userNameAttributeName); this.idToken = idToken; @@ -118,7 +121,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority { * {@code null}. * @return the {@link OidcUserInfo} containing claims about the user, or {@code null} */ - public OidcUserInfo getUserInfo() { + public @Nullable OidcUserInfo getUserInfo() { return this.userInfo; } @@ -137,8 +140,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority { if (!this.getIdToken().equals(that.getIdToken())) { return false; } - return (this.getUserInfo() != null) ? this.getUserInfo().equals(that.getUserInfo()) - : that.getUserInfo() == null; + return Objects.equals(this.getUserInfo(), that.getUserInfo()); } @Override @@ -149,7 +151,7 @@ public class OidcUserAuthority extends OAuth2UserAuthority { return result; } - static Map collectClaims(OidcIdToken idToken, OidcUserInfo userInfo) { + static Map collectClaims(OidcIdToken idToken, @Nullable OidcUserInfo userInfo) { Assert.notNull(idToken, "idToken cannot be null"); Map claims = new HashMap<>(); if (userInfo != null) { diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/package-info.java index e4146d8563..040dcb5ef6 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/package-info.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/package-info.java @@ -18,4 +18,7 @@ * Provides a model for an OpenID Connect Core 1.0 representation of a user * {@code Principal}. */ +@NullMarked package org.springframework.security.oauth2.core.oidc.user; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java index 3021617c09..aa7c3520d5 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/package-info.java @@ -18,4 +18,7 @@ * Core classes and interfaces providing support for the OAuth 2.0 Authorization * Framework. */ +@NullMarked package org.springframework.security.oauth2.core; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java index a8d09ec7aa..7d4a33cee2 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java @@ -27,6 +27,8 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.util.Assert; @@ -59,17 +61,17 @@ public class DefaultOAuth2User implements OAuth2User, Serializable { /** * Constructs a {@code DefaultOAuth2User} using the provided parameters. - * @param authorities the authorities granted to the user + * @param authorities the authorities granted to the user, may be {@code null} * @param attributes the attributes about the user * @param nameAttributeKey the key used to access the user's "name" from * {@link #getAttributes()} */ - public DefaultOAuth2User(Collection authorities, Map attributes, - String nameAttributeKey) { + public DefaultOAuth2User(@Nullable Collection authorities, + Map attributes, String nameAttributeKey) { Assert.notEmpty(attributes, "attributes cannot be empty"); Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty"); - Assert.notNull(attributes.get(nameAttributeKey), - "Attribute value for '" + nameAttributeKey + "' cannot be null"); + Object nameAttributeValue = attributes.get(nameAttributeKey); + Assert.notNull(nameAttributeValue, "Attribute value for '" + nameAttributeKey + "' cannot be null"); this.authorities = (authorities != null) ? Collections.unmodifiableSet(new LinkedHashSet<>(this.sortAuthorities(authorities))) @@ -80,7 +82,9 @@ public class DefaultOAuth2User implements OAuth2User, Serializable { @Override public String getName() { - return this.getAttribute(this.nameAttributeKey).toString(); + Object nameAttributeValue = this.getAttribute(this.nameAttributeKey); + Assert.notNull(nameAttributeValue, "Name attribute value cannot be null"); + return nameAttributeValue.toString(); } @Override diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java index 5a94b825d5..7d4a5751a2 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java @@ -22,7 +22,8 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; import org.springframework.util.Assert; @@ -41,7 +42,7 @@ public class OAuth2UserAuthority implements GrantedAuthority { private final Map attributes; - private final String userNameAttributeName; + private final @Nullable String userNameAttributeName; /** * Constructs a {@code OAuth2UserAuthority} using the provided parameters and defaults @@ -78,10 +79,11 @@ public class OAuth2UserAuthority implements GrantedAuthority { * @param authority the authority granted to the user * @param attributes the attributes about the user * @param userNameAttributeName the attribute name used to access the user's name from - * the attributes + * the attributes, may be {@code null} * @since 6.4 */ - public OAuth2UserAuthority(String authority, Map attributes, String userNameAttributeName) { + public OAuth2UserAuthority(String authority, Map attributes, + @Nullable String userNameAttributeName) { Assert.hasText(authority, "authority cannot be empty"); Assert.notEmpty(attributes, "attributes cannot be empty"); this.authority = authority; @@ -104,11 +106,11 @@ public class OAuth2UserAuthority implements GrantedAuthority { /** * Returns the attribute name used to access the user's name from the attributes. - * @return the attribute name used to access the user's name from the attributes + * @return the attribute name used to access the user's name from the attributes, or + * {@code null} if not available * @since 6.4 */ - @Nullable - public String getUserNameAttributeName() { + public @Nullable String getUserNameAttributeName() { return this.userNameAttributeName; } @@ -137,8 +139,9 @@ public class OAuth2UserAuthority implements GrantedAuthority { } } else { - Object thatValue = convertURLIfNecessary(thatAttributes.get(key)); - if (!value.equals(thatValue)) { + Object thatValue = thatAttributes.get(key); + Object convertedThatValue = convertURLIfNecessary(thatValue); + if (!value.equals(convertedThatValue)) { return false; } } @@ -165,9 +168,10 @@ public class OAuth2UserAuthority implements GrantedAuthority { /** * @return {@code URL} converted to a string since {@code URL} shouldn't be used for - * equality/hashCode. For other instances the value is returned as is. + * equality/hashCode. For other instances the value is returned as is (including + * null). */ - private static Object convertURLIfNecessary(Object value) { + private static @Nullable Object convertURLIfNecessary(@Nullable Object value) { return (value instanceof URL) ? ((URL) value).toExternalForm() : value; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java index d0c4a13079..48ca8909c9 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/package-info.java @@ -17,4 +17,7 @@ /** * Provides a model for an OAuth 2.0 representation of a user {@code Principal}. */ +@NullMarked package org.springframework.security.oauth2.core.user; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/package-info.java new file mode 100644 index 0000000000..ae9c62758e --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/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. + */ + +/** + * Web support classes for OAuth 2.0 and OpenID Connect. + */ +@NullMarked +package org.springframework.security.oauth2.core.web; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/package-info.java new file mode 100644 index 0000000000..e94035ff55 --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/function/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 functional web support classes for OAuth 2.0 and OpenID Connect. + */ +@NullMarked +package org.springframework.security.oauth2.core.web.reactive.function; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/package-info.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/package-info.java new file mode 100644 index 0000000000..9b69c6968c --- /dev/null +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/web/reactive/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 web support classes for OAuth 2.0 and OpenID Connect. + */ +@NullMarked +package org.springframework.security.oauth2.core.web.reactive; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java index d1fef9d1f8..9ae84282d1 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java @@ -76,11 +76,11 @@ public class BearerTokenAuthenticationTests { } @Test - public void getNameWhenHasNoSubjectThenReturnsNull() { + public void getNameWhenHasNoSubjectThenReturnsEmptyString() { OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal( Collections.singletonMap("claim", "value"), null); BearerTokenAuthentication authenticated = new BearerTokenAuthentication(principal, this.token, null); - assertThat(authenticated.getName()).isNull(); + assertThat(authenticated.getName()).isEmpty(); } @Test