diff --git a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle index dabe11e63d..6111da5d56 100644 --- a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle +++ b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle @@ -1,5 +1,9 @@ +plugins { + id 'security-nullability' + id 'javadoc-warnings-error' +} + apply plugin: 'io.spring.convention.spring-module' -apply plugin: 'javadoc-warnings-error' dependencies { management platform(project(":spring-security-dependencies")) diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java index d06348e43b..267280c925 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java @@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource; import java.io.Serial; +import org.jspecify.annotations.Nullable; + import org.springframework.http.HttpStatus; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.util.Assert; @@ -41,14 +43,15 @@ public final class BearerTokenError extends OAuth2Error { private final HttpStatus httpStatus; - private final String scope; + private final @Nullable String scope; /** * Create a {@code BearerTokenError} using the provided parameters * @param errorCode the error code * @param httpStatus the HTTP status */ - public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri) { + public BearerTokenError(String errorCode, HttpStatus httpStatus, @Nullable String description, + @Nullable String errorUri) { this(errorCode, httpStatus, description, errorUri, null); } @@ -60,8 +63,8 @@ public final class BearerTokenError extends OAuth2Error { * @param errorUri the URI * @param scope the scope */ - public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri, - String scope) { + public BearerTokenError(String errorCode, HttpStatus httpStatus, @Nullable String description, + @Nullable String errorUri, @Nullable String scope) { super(errorCode, description, errorUri); Assert.notNull(httpStatus, "httpStatus cannot be null"); Assert.isTrue(isDescriptionValid(description), @@ -85,13 +88,13 @@ public final class BearerTokenError extends OAuth2Error { /** * Return the scope. - * @return the scope + * @return the scope, or {@code null} if not set */ - public String getScope() { + public @Nullable String getScope() { return this.scope; } - private static boolean isDescriptionValid(String description) { + private static boolean isDescriptionValid(@Nullable String description) { // @formatter:off return description == null || description.chars().allMatch((c) -> withinTheRangeOf(c, 0x20, 0x21) || @@ -109,12 +112,12 @@ public final class BearerTokenError extends OAuth2Error { // @formatter:on } - private static boolean isErrorUriValid(String errorUri) { + private static boolean isErrorUriValid(@Nullable String errorUri) { return errorUri == null || errorUri.chars() .allMatch((c) -> c == 0x21 || withinTheRangeOf(c, 0x23, 0x5B) || withinTheRangeOf(c, 0x5D, 0x7E)); } - private static boolean isScopeValid(String scope) { + private static boolean isScopeValid(@Nullable String scope) { // @formatter:off return scope == null || scope.chars().allMatch((c) -> withinTheRangeOf(c, 0x20, 0x21) || diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrors.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrors.java index fecb258801..1f010b37dd 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrors.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenErrors.java @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.server.resource; +import org.jspecify.annotations.Nullable; + import org.springframework.http.HttpStatus; /** @@ -78,7 +80,7 @@ public final class BearerTokenErrors { * @param scope the scope attribute to use in the error * @return a {@link BearerTokenError} */ - public static BearerTokenError insufficientScope(String message, String scope) { + public static BearerTokenError insufficientScope(String message, @Nullable String scope) { try { return new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN, message, DEFAULT_URI, scope); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/InvalidBearerTokenException.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/InvalidBearerTokenException.java index fcd021799a..d4bd22f7ca 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/InvalidBearerTokenException.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/InvalidBearerTokenException.java @@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource; import java.io.Serial; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.OAuth2AuthenticationException; /** @@ -52,10 +54,13 @@ public class InvalidBearerTokenException extends OAuth2AuthenticationException { * {@link org.springframework.security.oauth2.core.OAuth2Error} instance as the * {@code error_description}. * @param description the description - * @param cause the causing exception + * @param cause the causing exception, or {@code null} */ - public InvalidBearerTokenException(String description, Throwable cause) { - super(BearerTokenErrors.invalidToken(description), cause); + public InvalidBearerTokenException(String description, @Nullable Throwable cause) { + super(BearerTokenErrors.invalidToken(description)); + if (cause != null) { + initCause(cause); + } } } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/OAuth2ProtectedResourceMetadata.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/OAuth2ProtectedResourceMetadata.java index 7e66520872..ef4dd373d0 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/OAuth2ProtectedResourceMetadata.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/OAuth2ProtectedResourceMetadata.java @@ -27,6 +27,8 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -271,7 +273,10 @@ public final class OAuth2ProtectedResourceMetadata valuesConsumer.accept(values); } - private static void validateURL(Object url, String errorMessage) { + private static void validateURL(@Nullable Object url, String errorMessage) { + if (url == null) { + return; + } if (URL.class.isAssignableFrom(url.getClass())) { return; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/OAuth2ProtectedResourceMetadataClaimAccessor.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/OAuth2ProtectedResourceMetadataClaimAccessor.java index 10fcb54497..2f1aac1901 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/OAuth2ProtectedResourceMetadataClaimAccessor.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/OAuth2ProtectedResourceMetadataClaimAccessor.java @@ -19,9 +19,13 @@ package org.springframework.security.oauth2.server.resource; import java.net.URI; import java.net.URL; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.util.Assert; /** * A {@link ClaimAccessor} for the claims a Resource Server describes about its @@ -42,7 +46,9 @@ public interface OAuth2ProtectedResourceMetadataClaimAccessor extends ClaimAcces * @return the {@code URL} the protected resource asserts as its resource identifier */ default URL getResource() { - return getClaimAsURL(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE); + URL resource = getClaimAsURL(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE); + Assert.notNull(resource, "resource cannot be null"); + return resource; } /** @@ -50,11 +56,14 @@ public interface OAuth2ProtectedResourceMetadataClaimAccessor extends ClaimAcces * servers that can be used with this protected resource * {@code (authorization_servers)}. * @return a list of {@code issuer} identifier {@code URL}'s, for authorization - * servers that can be used with this protected resource + * servers that can be used with this protected resource, or an empty list if not set */ default List getAuthorizationServers() { List authorizationServers = getClaimAsStringList( OAuth2ProtectedResourceMetadataClaimNames.AUTHORIZATION_SERVERS); + if (authorizationServers == null) { + return Collections.emptyList(); + } List urls = new ArrayList<>(); authorizationServers.forEach((authorizationServer) -> { try { @@ -70,11 +79,11 @@ public interface OAuth2ProtectedResourceMetadataClaimAccessor extends ClaimAcces /** * Returns a list of {@code scope} values supported, that are used in authorization * requests to request access to this protected resource {@code (scopes_supported)}. - * @return a list of {@code scope} values supported, that are used in authorization - * requests to request access to this protected resource + * @return a list of {@code scope} values supported, or an empty list if not set */ default List getScopes() { - return getClaimAsStringList(OAuth2ProtectedResourceMetadataClaimNames.SCOPES_SUPPORTED); + List scopes = getClaimAsStringList(OAuth2ProtectedResourceMetadataClaimNames.SCOPES_SUPPORTED); + return (scopes != null) ? scopes : Collections.emptyList(); } /** @@ -82,18 +91,20 @@ public interface OAuth2ProtectedResourceMetadataClaimAccessor extends ClaimAcces * the protected resource. Defined values are "header", "body" and "query". * {@code (bearer_methods_supported)}. * @return a list of the supported methods for sending an OAuth 2.0 bearer token to - * the protected resource + * the protected resource, or an empty list if not set */ default List getBearerMethodsSupported() { - return getClaimAsStringList(OAuth2ProtectedResourceMetadataClaimNames.BEARER_METHODS_SUPPORTED); + List methods = getClaimAsStringList(OAuth2ProtectedResourceMetadataClaimNames.BEARER_METHODS_SUPPORTED); + return (methods != null) ? methods : Collections.emptyList(); } /** * Returns the name of the protected resource intended for display to the end user * {@code (resource_name)}. - * @return the name of the protected resource intended for display to the end user + * @return the name of the protected resource intended for display to the end user, or + * {@code null} if not set */ - default String getResourceName() { + default @Nullable String getResourceName() { return getClaimAsString(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE_NAME); } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java index 86ae387e00..89777976d4 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java @@ -61,21 +61,20 @@ public abstract class AbstractOAuth2TokenAuthenticationToken authorities) { - + protected AbstractOAuth2TokenAuthenticationToken(T token, + @Nullable Collection authorities) { this(token, token, token, authorities); } protected AbstractOAuth2TokenAuthenticationToken(T token, Object principal, Object credentials, - Collection authorities) { + @Nullable Collection authorities) { super(authorities); Assert.notNull(token, "token cannot be null"); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider.java index d3db0bdfe2..361dec2113 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/DPoPAuthenticationProvider.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.function.Function; import com.nimbusds.jose.jwk.JWK; +import org.jspecify.annotations.Nullable; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; @@ -72,13 +73,15 @@ public final class DPoPAuthenticationProvider implements AuthenticationProvider public DPoPAuthenticationProvider(AuthenticationManager tokenAuthenticationManager) { Assert.notNull(tokenAuthenticationManager, "tokenAuthenticationManager cannot be null"); this.tokenAuthenticationManager = tokenAuthenticationManager; - Function> jwtValidatorFactory = ( - context) -> new DelegatingOAuth2TokenValidator<>( - // Use default validators - DPoPProofJwtDecoderFactory.DEFAULT_JWT_VALIDATOR_FACTORY.apply(context), - // Add custom validators - new AthClaimValidator(context.getAccessToken()), - new JwkThumbprintValidator(context.getAccessToken())); + Function> jwtValidatorFactory = (context) -> { + OAuth2AccessTokenClaims accessToken = context.getAccessToken(); + Assert.notNull(accessToken, "accessToken cannot be null"); + return new DelegatingOAuth2TokenValidator<>( + // Use default validators + DPoPProofJwtDecoderFactory.DEFAULT_JWT_VALIDATOR_FACTORY.apply(context), + // Add custom validators + new AthClaimValidator(accessToken), new JwkThumbprintValidator(accessToken)); + }; DPoPProofJwtDecoderFactory dPoPProofJwtDecoderFactory = new DPoPProofJwtDecoderFactory(); dPoPProofJwtDecoderFactory.setJwtValidatorFactory(jwtValidatorFactory); this.dPoPProofVerifierFactory = dPoPProofJwtDecoderFactory; @@ -260,12 +263,12 @@ public final class DPoPAuthenticationProvider implements AuthenticationProvider } @Override - public Instant getIssuedAt() { + public @Nullable Instant getIssuedAt() { return this.accessToken.getIssuedAt(); } @Override - public Instant getExpiresAt() { + public @Nullable Instant getExpiresAt() { return this.accessToken.getExpiresAt(); } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java index 2b778dec13..089c57b0e0 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java @@ -114,7 +114,8 @@ public class JwtAuthenticationConverter implements Converter authorities) { super(jwt, principal, jwt, authorities); this.setAuthenticated(true); - if (principal instanceof AuthenticatedPrincipal) { - this.name = ((AuthenticatedPrincipal) principal).getName(); + if (principal instanceof AuthenticatedPrincipal authenticatedPrincipal) { + this.name = authenticatedPrincipal.getName(); } else { this.name = jwt.getSubject(); @@ -106,11 +106,12 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok } /** - * The principal name which is, by default, the {@link Jwt}'s subject + * The principal name which is, by default, the {@link Jwt}'s subject. Returns empty + * string if the subject claim is absent. */ @Override public String getName() { - return this.name; + return (this.name != null) ? this.name : ""; } @Override @@ -126,7 +127,7 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok */ public static class Builder> extends AbstractOAuth2TokenAuthenticationBuilder { - private String name; + private @Nullable String name; protected Builder(JwtAuthenticationToken token) { super(token); @@ -168,10 +169,10 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok /** * The name to use. - * @param name the name to use + * @param name the name to use, or {@code null} if the principal has no name * @return the {@link Builder} for further configurations */ - public B name(String name) { + public B name(@Nullable String name) { this.name = name; return (B) this; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java index 6ca21b2bdb..6efa8a77da 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverter.java @@ -23,6 +23,7 @@ import java.util.Collections; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.core.log.LogMessage; @@ -105,7 +106,7 @@ public final class JwtGrantedAuthoritiesConverter implements Converter { @Override - public String convert(@NonNull BearerTokenAuthenticationToken authentication) { + public String convert(BearerTokenAuthenticationToken authentication) { String token = authentication.getToken(); try { String issuer = JWTParser.parse(token).getJWTClaimsSet().getIssuer(); @@ -178,7 +177,8 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat } } catch (Exception cause) { - AuthenticationException ex = new InvalidBearerTokenException(cause.getMessage(), cause); + AuthenticationException ex = new InvalidBearerTokenException( + (cause.getMessage() != null) ? cause.getMessage() : "Invalid token", cause); ex.setAuthenticationRequest(authentication); throw ex; } @@ -202,6 +202,9 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat } @Override + @SuppressWarnings("NullAway") // Interface does not declare @Nullable; this + // implementation returns null when issuer not + // trusted public AuthenticationManager resolve(String issuer) { if (this.trustedIssuer.test(issuer)) { AuthenticationManager authenticationManager = this.authenticationManagers.computeIfAbsent(issuer, diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java index 1913156b5b..817c833ea3 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java @@ -31,7 +31,6 @@ import reactor.core.scheduler.Schedulers; import org.springframework.core.convert.converter.Converter; import org.springframework.core.log.LogMessage; -import org.springframework.lang.NonNull; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; @@ -172,7 +171,7 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver private static class JwtClaimIssuerConverter implements Converter> { @Override - public Mono convert(@NonNull BearerTokenAuthenticationToken token) { + public Mono convert(BearerTokenAuthenticationToken token) { try { String issuer = JWTParser.parse(token.getToken()).getJWTClaimsSet().getIssuer(); if (issuer == null) { @@ -184,7 +183,8 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver } catch (Exception cause) { return Mono.error(() -> { - AuthenticationException ex = new InvalidBearerTokenException(cause.getMessage(), cause); + AuthenticationException ex = new InvalidBearerTokenException( + (cause.getMessage() != null) ? cause.getMessage() : "Invalid token", cause); ex.setAuthenticationRequest(token); return ex; }); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java index 47f904e181..b7480f3275 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtReactiveAuthenticationManager.java @@ -76,7 +76,7 @@ public final class JwtReactiveAuthenticationManager implements ReactiveAuthentic private AuthenticationException onError(JwtException ex) { if (ex instanceof BadJwtException) { - return new InvalidBearerTokenException(ex.getMessage(), ex); + return new InvalidBearerTokenException((ex.getMessage() != null) ? ex.getMessage() : "Invalid token", ex); } return new AuthenticationServiceException(ex.getMessage(), ex); } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java index 0c97b1d429..e103d3ba9c 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.java @@ -22,6 +22,7 @@ import java.util.HashSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationProvider; @@ -104,7 +105,7 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr * @throws AuthenticationException if authentication failed for some reason */ @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { + public @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException { if (!(authentication instanceof BearerTokenAuthenticationToken bearer)) { return null; } @@ -129,7 +130,8 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr } catch (BadOpaqueTokenException failed) { this.logger.debug("Failed to authenticate since token was invalid"); - throw new InvalidBearerTokenException(failed.getMessage(), failed); + throw new InvalidBearerTokenException((failed.getMessage() != null) ? failed.getMessage() : "Invalid token", + failed); } catch (OAuth2IntrospectionException failed) { throw new AuthenticationServiceException(failed.getMessage(), failed); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java index ce2d2dc634..2601269245 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManager.java @@ -105,7 +105,7 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent private AuthenticationException onError(OAuth2IntrospectionException ex) { if (ex instanceof BadOpaqueTokenException) { - return new InvalidBearerTokenException(ex.getMessage(), ex); + return new InvalidBearerTokenException((ex.getMessage() != null) ? ex.getMessage() : "Invalid token", ex); } return new AuthenticationServiceException(ex.getMessage(), ex); } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java index 348a48a04c..f48bfd752c 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/ReactiveJwtAuthenticationConverter.java @@ -48,7 +48,8 @@ public final class ReactiveJwtAuthenticationConverter implements Converter { String principalName = jwt.getClaimAsString(this.principalClaimName); - return new JwtAuthenticationToken(jwt, authorities, principalName); + return new JwtAuthenticationToken(jwt, authorities, + (principalName != null) ? principalName : ""); }); // @formatter:on } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java index f2135ac9a9..c188c8807a 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/package-info.java @@ -18,4 +18,7 @@ * OAuth 2.0 Resource Server {@code Authentication}s and supporting classes and * interfaces. */ +@NullMarked package org.springframework.security.oauth2.server.resource.authentication; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/BadOpaqueTokenException.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/BadOpaqueTokenException.java index a674de2950..231fe20d5b 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/BadOpaqueTokenException.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/BadOpaqueTokenException.java @@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.introspection; import java.io.Serial; +import org.jspecify.annotations.Nullable; + /** * An exception similar to * {@link org.springframework.security.authentication.BadCredentialsException} that @@ -35,7 +37,7 @@ public class BadOpaqueTokenException extends OAuth2IntrospectionException { super(message); } - public BadOpaqueTokenException(String message, Throwable cause) { + public BadOpaqueTokenException(String message, @Nullable Throwable cause) { super(message, cause); } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionException.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionException.java index 0fffc9718e..594573a8bb 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionException.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionException.java @@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.introspection; import java.io.Serial; +import org.jspecify.annotations.Nullable; + /** * Base exception for all OAuth 2.0 Introspection related errors * @@ -33,7 +35,7 @@ public class OAuth2IntrospectionException extends RuntimeException { super(message); } - public OAuth2IntrospectionException(String message, Throwable cause) { + public OAuth2IntrospectionException(String message, @Nullable Throwable cause) { super(message, cause); } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/RestClientOpaqueTokenIntrospector.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/RestClientOpaqueTokenIntrospector.java index dfdce8d8d7..4cef39c7b1 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/RestClientOpaqueTokenIntrospector.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/RestClientOpaqueTokenIntrospector.java @@ -31,6 +31,7 @@ import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.convert.converter.Converter; @@ -108,7 +109,7 @@ public final class RestClientOpaqueTokenIntrospector implements OpaqueTokenIntro return spec.retrieve().toEntity(STRING_OBJECT_MAP); } catch (Exception ex) { - throw new OAuth2IntrospectionException(ex.getMessage(), ex); + throw new OAuth2IntrospectionException((ex.getMessage() != null) ? ex.getMessage() : "Invalid token", ex); } } @@ -208,7 +209,8 @@ public final class RestClientOpaqueTokenIntrospector implements OpaqueTokenIntro */ private OAuth2IntrospectionAuthenticatedPrincipal defaultAuthenticationConverter( OAuth2TokenIntrospectionClaimAccessor accessor) { - Collection authorities = authorities(accessor.getScopes()); + List scopes = accessor.getScopes(); + Collection authorities = authorities((scopes != null) ? scopes : Collections.emptyList()); return new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities); } @@ -250,7 +252,7 @@ public final class RestClientOpaqueTokenIntrospector implements OpaqueTokenIntro private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor { @Override - default List getScopes() { + default @Nullable List getScopes() { Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE); if (value instanceof ArrayListFromString list) { return list; @@ -270,9 +272,9 @@ public final class RestClientOpaqueTokenIntrospector implements OpaqueTokenIntro private final String introspectionUri; - private String clientId; + private @Nullable String clientId; - private String clientSecret; + private @Nullable String clientSecret; private final List> postProcessors = new ArrayList<>(); @@ -322,9 +324,13 @@ public final class RestClientOpaqueTokenIntrospector implements OpaqueTokenIntro * @return the {@link RestClientOpaqueTokenIntrospector} */ public RestClientOpaqueTokenIntrospector build() { - RestClient restClient = RestClient.builder() - .defaultHeaders((headers) -> headers.setBasicAuth(this.clientId, this.clientSecret)) - .build(); + RestClient.Builder builder = RestClient.builder(); + if (this.clientId != null && this.clientSecret != null) { + String clientId = this.clientId; + String clientSecret = this.clientSecret; + builder.defaultHeaders((headers) -> headers.setBasicAuth(clientId, clientSecret)); + } + RestClient restClient = builder.build(); RestClientOpaqueTokenIntrospector introspector = new RestClientOpaqueTokenIntrospector( this.introspectionUri, restClient); this.postProcessors.forEach((postProcessor) -> postProcessor.accept(introspector)); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java index df0de4bb99..459036151b 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java @@ -32,6 +32,7 @@ import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.convert.converter.Converter; @@ -157,7 +158,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector { return this.restOperations.exchange(requestEntity, STRING_OBJECT_MAP); } catch (Exception ex) { - throw new OAuth2IntrospectionException(ex.getMessage(), ex); + throw new OAuth2IntrospectionException((ex.getMessage() != null) ? ex.getMessage() : "Invalid token", ex); } } @@ -259,7 +260,8 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector { */ private OAuth2IntrospectionAuthenticatedPrincipal defaultAuthenticationConverter( OAuth2TokenIntrospectionClaimAccessor accessor) { - Collection authorities = authorities(accessor.getScopes()); + List scopes = accessor.getScopes(); + Collection authorities = authorities((scopes != null) ? scopes : Collections.emptyList()); return new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities); } @@ -302,7 +304,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector { private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor { @Override - default List getScopes() { + default @Nullable List getScopes() { Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE); if (value instanceof ArrayListFromString list) { return list; @@ -322,9 +324,9 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector { private final String introspectionUri; - private String clientId; + private @Nullable String clientId; - private String clientSecret; + private @Nullable String clientSecret; private final List> postProcessors = new ArrayList<>(); @@ -379,7 +381,10 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector { */ public SpringOpaqueTokenIntrospector build() { RestTemplate restTemplate = new RestTemplate(); - restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(this.clientId, this.clientSecret)); + if (this.clientId != null && this.clientSecret != null) { + restTemplate.getInterceptors() + .add(new BasicAuthenticationInterceptor(this.clientId, this.clientSecret)); + } SpringOpaqueTokenIntrospector introspector = new SpringOpaqueTokenIntrospector(this.introspectionUri, restTemplate); this.postProcessors.forEach((postProcessor) -> postProcessor.accept(introspector)); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java index ed6dbcaad9..77b95aca7b 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospector.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; @@ -188,7 +189,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke } private OAuth2IntrospectionException onError(Throwable ex) { - return new OAuth2IntrospectionException(ex.getMessage(), ex); + return new OAuth2IntrospectionException((ex.getMessage() != null) ? ex.getMessage() : "Invalid token", ex); } /** @@ -212,7 +213,8 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke private Mono defaultAuthenticationConverter( OAuth2TokenIntrospectionClaimAccessor accessor) { - Collection authorities = authorities(accessor.getScopes()); + List scopes = accessor.getScopes(); + Collection authorities = authorities((scopes != null) ? scopes : Collections.emptyList()); return Mono.just(new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities)); } @@ -255,7 +257,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor { @Override - default List getScopes() { + default @Nullable List getScopes() { Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE); if (value instanceof ArrayListFromString list) { return list; @@ -275,9 +277,9 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke private final String introspectionUri; - private String clientId; + private @Nullable String clientId; - private String clientSecret; + private @Nullable String clientSecret; private final List> postProcessors = new ArrayList<>(); @@ -332,9 +334,13 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke * @since 6.5 */ public SpringReactiveOpaqueTokenIntrospector build() { - WebClient webClient = WebClient.builder() - .defaultHeaders((h) -> h.setBasicAuth(this.clientId, this.clientSecret)) - .build(); + WebClient.Builder builder = WebClient.builder(); + if (this.clientId != null && this.clientSecret != null) { + String clientId = this.clientId; + String clientSecret = this.clientSecret; + builder.defaultHeaders((h) -> h.setBasicAuth(clientId, clientSecret)); + } + WebClient webClient = builder.build(); SpringReactiveOpaqueTokenIntrospector introspector = new SpringReactiveOpaqueTokenIntrospector( this.introspectionUri, webClient); this.postProcessors.forEach((postProcessor) -> postProcessor.accept(introspector)); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/package-info.java index 1407e244b4..6a1aa9d3b0 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/package-info.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/package-info.java @@ -17,4 +17,7 @@ /** * OAuth 2.0 Introspection supporting classes and interfaces. */ +@NullMarked package org.springframework.security.oauth2.server.resource.introspection; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java index 02ddff69a4..4161f18ba2 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/package-info.java @@ -17,4 +17,7 @@ /** * OAuth 2.0 Resource Server core classes and interfaces providing support. */ +@NullMarked package org.springframework.security.oauth2.server.resource; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java index 157efbc9a3..b08cf11434 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java @@ -22,6 +22,7 @@ import java.util.function.Function; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -51,7 +52,7 @@ import org.springframework.web.util.UriComponentsBuilder; */ public final class BearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint { - private String realmName; + private @Nullable String realmName; private Function resourceMetadataParameterResolver = BearerTokenAuthenticationEntryPoint::getResourceMetadataParameter; @@ -95,9 +96,9 @@ public final class BearerTokenAuthenticationEntryPoint implements Authentication /** * Set the default realm name to use in the bearer token error response - * @param realmName + * @param realmName the realm name, or {@code null} */ - public void setRealmName(String realmName) { + public void setRealmName(@Nullable String realmName) { this.realmName = realmName; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java index 4d7debc3bd..d95c92f349 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenResolver.java @@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.resource.web; import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -41,6 +42,6 @@ public interface BearerTokenResolver { * @return the Bearer Token value or {@code null} if none found * @throws OAuth2AuthenticationException if the found token is invalid */ - String resolve(HttpServletRequest request); + @Nullable String resolve(HttpServletRequest request); } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java index afee8594fb..eaa2f3ecaa 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java @@ -20,6 +20,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -51,7 +52,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver { private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION; @Override - public String resolve(final HttpServletRequest request) { + public @Nullable String resolve(final HttpServletRequest request) { // @formatter:off return resolveToken( resolveFromAuthorizationHeader(request), @@ -61,7 +62,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver { // @formatter:on } - private static String resolveToken(String... accessTokens) { + private static @Nullable String resolveToken(@Nullable String... accessTokens) { if (accessTokens == null || accessTokens.length == 0) { return null; } @@ -87,7 +88,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver { return accessToken; } - private String resolveFromAuthorizationHeader(HttpServletRequest request) { + private @Nullable String resolveFromAuthorizationHeader(HttpServletRequest request) { String authorization = request.getHeader(this.bearerTokenHeaderName); if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) { return null; @@ -102,7 +103,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver { return matcher.group("token"); } - private String resolveAccessTokenFromQueryString(HttpServletRequest request) { + private @Nullable String resolveAccessTokenFromQueryString(HttpServletRequest request) { if (!this.allowUriQueryParameter || !HttpMethod.GET.name().equals(request.getMethod())) { return null; } @@ -110,7 +111,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver { return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)); } - private String resolveAccessTokenFromBody(HttpServletRequest request) { + private @Nullable String resolveAccessTokenFromBody(HttpServletRequest request) { if (!this.allowFormEncodedBodyParameter || !MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()) || HttpMethod.GET.name().equals(request.getMethod())) { diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/OAuth2ProtectedResourceMetadataFilter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/OAuth2ProtectedResourceMetadataFilter.java index d5168e1150..79b7d07c3a 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/OAuth2ProtectedResourceMetadataFilter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/OAuth2ProtectedResourceMetadataFilter.java @@ -24,6 +24,7 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; @@ -60,7 +61,7 @@ public final class OAuth2ProtectedResourceMetadataFilter extends OncePerRequestF private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() { }; - private static final GenericHttpMessageConverter JSON_MESSAGE_CONVERTER = HttpMessageConverters + private static final @Nullable GenericHttpMessageConverter JSON_MESSAGE_CONVERTER = HttpMessageConverters .getJsonMessageConverter(); /** @@ -108,8 +109,10 @@ public final class OAuth2ProtectedResourceMetadataFilter extends OncePerRequestF OAuth2ProtectedResourceMetadata protectedResourceMetadata = builder.build(); try { + GenericHttpMessageConverter converter = JSON_MESSAGE_CONVERTER; + Assert.notNull(converter, "No JSON message converter available"); ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); - JSON_MESSAGE_CONVERTER.write(protectedResourceMetadata.getClaims(), STRING_OBJECT_MAP.getType(), + converter.write(protectedResourceMetadata.getClaims(), STRING_OBJECT_MAP.getType(), MediaType.APPLICATION_JSON, httpResponse); } catch (Exception ex) { @@ -161,7 +164,7 @@ public final class OAuth2ProtectedResourceMetadataFilter extends OncePerRequestF } @SuppressWarnings("removal") - private static GenericHttpMessageConverter getJsonMessageConverter() { + private static @Nullable GenericHttpMessageConverter getJsonMessageConverter() { if (jacksonPresent) { return new GenericHttpMessageConverterAdapter<>(new JacksonJsonHttpMessageConverter()); } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java index f80185f4c5..bddc6d1360 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/BearerTokenAccessDeniedHandler.java @@ -21,6 +21,7 @@ import java.util.Map; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -46,7 +47,7 @@ import org.springframework.security.web.access.AccessDeniedHandler; */ public final class BearerTokenAccessDeniedHandler implements AccessDeniedHandler { - private String realmName; + private @Nullable String realmName; /** * Collect error details from the provided parameters and format according to RFC @@ -78,7 +79,7 @@ public final class BearerTokenAccessDeniedHandler implements AccessDeniedHandler * Set the default realm name to use in the bearer token error response * @param realmName */ - public void setRealmName(String realmName) { + public void setRealmName(@Nullable String realmName) { this.realmName = realmName; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java index 87a7364d42..5f364e7666 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/package-info.java @@ -17,4 +17,7 @@ /** * OAuth 2.0 Resource Server access denial classes and interfaces. */ +@NullMarked package org.springframework.security.oauth2.server.resource.web.access; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandler.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandler.java index f135d035b7..d2b2bf6995 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandler.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/BearerTokenServerAccessDeniedHandler.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; @@ -51,7 +52,7 @@ public class BearerTokenServerAccessDeniedHandler implements ServerAccessDeniedH private static final Collection WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = Arrays.asList("scope", "scp"); - private String realmName; + private @Nullable String realmName; @Override public Mono handle(ServerWebExchange exchange, AccessDeniedException denied) { @@ -72,7 +73,7 @@ public class BearerTokenServerAccessDeniedHandler implements ServerAccessDeniedH * Set the default realm name to use in the bearer token error response * @param realmName */ - public final void setRealmName(String realmName) { + public final void setRealmName(@Nullable String realmName) { this.realmName = realmName; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/package-info.java new file mode 100644 index 0000000000..7d8648b4ed --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/access/server/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OAuth 2.0 Resource Server WebFlux access denied handlers. + */ +@NullMarked +package org.springframework.security.oauth2.server.resource.web.access.server; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java index 3d016041b0..340de5bbe9 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationConverter.java @@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.resource.web.authentication; import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.core.Authentication; @@ -43,7 +44,7 @@ public final class BearerTokenAuthenticationConverter implements AuthenticationC private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); @Override - public Authentication convert(HttpServletRequest request) { + public @Nullable Authentication convert(HttpServletRequest request) { String token = this.bearerTokenResolver.resolve(request); if (StringUtils.hasText(token)) { BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(token); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/package-info.java new file mode 100644 index 0000000000..04381ed733 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OAuth 2.0 Resource Server web authentication converters and filters. + */ +@NullMarked +package org.springframework.security.oauth2.server.resource.web.authentication; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java index c68d9e1eba..18e8a0cdbe 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/package-info.java @@ -17,4 +17,7 @@ /** * OAuth 2.0 Resource Server {@code Filter}'s and supporting classes and interfaces. */ +@NullMarked package org.springframework.security.oauth2.server.resource.web; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.java index b59663654b..c5489b3e43 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.java @@ -20,8 +20,8 @@ import reactor.core.publisher.Mono; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.util.Assert; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; @@ -63,17 +63,18 @@ public final class ServerBearerExchangeFilterFunction implements ExchangeFilterF private Mono oauth2Token() { // @formatter:off return currentAuthentication() - .filter((authentication) -> authentication.getCredentials() instanceof OAuth2Token) - .map(Authentication::getCredentials) - .cast(OAuth2Token.class); + .filter((authentication) -> authentication.getCredentials() != null + && authentication.getCredentials() instanceof OAuth2Token) + .map((authentication) -> { + Object credentials = authentication.getCredentials(); + Assert.notNull(credentials, "credentials cannot be null"); + return (OAuth2Token) credentials; + }); // @formatter:on } private Mono currentAuthentication() { - // @formatter:off - return ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication); - // @formatter:on + return ReactiveSecurityContextHolder.getContext().flatMap((ctx) -> Mono.justOrEmpty(ctx.getAuthentication())); } private ClientRequest bearer(ClientRequest request, OAuth2Token token) { diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.java index 602f8b12eb..54d7e43a31 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.java @@ -18,11 +18,13 @@ package org.springframework.security.oauth2.server.resource.web.reactive.functio import java.util.Map; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import reactor.util.context.Context; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.util.Assert; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; @@ -71,9 +73,13 @@ public final class ServletBearerExchangeFilterFunction implements ExchangeFilter return Mono.deferContextual(Mono::just) .cast(Context.class) .flatMap(this::currentAuthentication) - .filter((authentication) -> authentication.getCredentials() instanceof OAuth2Token) - .map(Authentication::getCredentials) - .cast(OAuth2Token.class); + .filter((authentication) -> authentication.getCredentials() != null + && authentication.getCredentials() instanceof OAuth2Token) + .map((authentication) -> { + Object credentials = authentication.getCredentials(); + Assert.notNull(credentials, "credentials cannot be null"); + return (OAuth2Token) credentials; + }); // @formatter:on } @@ -81,7 +87,7 @@ public final class ServletBearerExchangeFilterFunction implements ExchangeFilter return Mono.justOrEmpty(getAttribute(ctx, Authentication.class)); } - private T getAttribute(Context ctx, Class clazz) { + private @Nullable T getAttribute(Context ctx, Class clazz) { // NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds // this key if (!ctx.hasKey(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY)) { diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/package-info.java new file mode 100644 index 0000000000..cbeda7aecf --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OAuth 2.0 Resource Server WebClient exchange filter functions. + */ +@NullMarked +package org.springframework.security.oauth2.server.resource.web.reactive.function.client; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java index 216a885df5..b8ac345508 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/BearerTokenServerAuthenticationEntryPoint.java @@ -19,6 +19,7 @@ package org.springframework.security.oauth2.server.resource.web.server; import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; @@ -49,9 +50,9 @@ import org.springframework.web.server.ServerWebExchange; */ public final class BearerTokenServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint { - private String realmName; + private @Nullable String realmName; - public void setRealmName(String realmName) { + public void setRealmName(@Nullable String realmName) { this.realmName = realmName; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/authentication/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/authentication/package-info.java new file mode 100644 index 0000000000..38958fcf6f --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/authentication/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OAuth 2.0 Resource Server WebFlux authentication converters. + */ +@NullMarked +package org.springframework.security.oauth2.server.resource.web.server.authentication; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/package-info.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/package-info.java new file mode 100644 index 0000000000..4cf50ff9f7 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OAuth 2.0 Resource Server WebFlux support classes. + */ +@NullMarked +package org.springframework.security.oauth2.server.resource.web.server; + +import org.jspecify.annotations.NullMarked; diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java index 4105dbf69a..62b5d857d7 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java @@ -47,10 +47,10 @@ public class JwtAuthenticationTokenTests { } @Test - public void getNameWhenJwtHasNoSubjectThenReturnsNull() { + public void getNameWhenJwtHasNoSubjectThenReturnsEmptyString() { Jwt jwt = builder().claim("claim", "value").build(); JwtAuthenticationToken token = new JwtAuthenticationToken(jwt); - assertThat(token.getName()).isNull(); + assertThat(token.getName()).isEmpty(); } @Test @@ -108,12 +108,12 @@ public class JwtAuthenticationTokenTests { } @Test - public void getNameWhenConstructedWithNoSubjectThenReturnsNull() { + public void getNameWhenConstructedWithNoSubjectThenReturnsEmptyString() { Collection authorities = AuthorityUtils.createAuthorityList("test"); Jwt jwt = builder().claim("claim", "value").build(); - assertThat(new JwtAuthenticationToken(jwt, authorities, (String) null).getName()).isNull(); - assertThat(new JwtAuthenticationToken(jwt, authorities).getName()).isNull(); - assertThat(new JwtAuthenticationToken(jwt).getName()).isNull(); + assertThat(new JwtAuthenticationToken(jwt, authorities, (String) null).getName()).isEmpty(); + assertThat(new JwtAuthenticationToken(jwt, authorities).getName()).isEmpty(); + assertThat(new JwtAuthenticationToken(jwt).getName()).isEmpty(); } @Test