1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Enable null-safety in spring-security-oauth2-resource-server

Closes gh-17822
This commit is contained in:
Joe Grandja
2026-03-18 16:50:54 -04:00
parent 1cb9db4f2d
commit 09ce639c4b
44 changed files with 333 additions and 122 deletions
@@ -1,5 +1,9 @@
plugins {
id 'security-nullability'
id 'javadoc-warnings-error'
}
apply plugin: 'io.spring.convention.spring-module' apply plugin: 'io.spring.convention.spring-module'
apply plugin: 'javadoc-warnings-error'
dependencies { dependencies {
management platform(project(":spring-security-dependencies")) management platform(project(":spring-security-dependencies"))
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource;
import java.io.Serial; import java.io.Serial;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@@ -41,14 +43,15 @@ public final class BearerTokenError extends OAuth2Error {
private final HttpStatus httpStatus; private final HttpStatus httpStatus;
private final String scope; private final @Nullable String scope;
/** /**
* Create a {@code BearerTokenError} using the provided parameters * Create a {@code BearerTokenError} using the provided parameters
* @param errorCode the error code * @param errorCode the error code
* @param httpStatus the HTTP status * @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); this(errorCode, httpStatus, description, errorUri, null);
} }
@@ -60,8 +63,8 @@ public final class BearerTokenError extends OAuth2Error {
* @param errorUri the URI * @param errorUri the URI
* @param scope the scope * @param scope the scope
*/ */
public BearerTokenError(String errorCode, HttpStatus httpStatus, String description, String errorUri, public BearerTokenError(String errorCode, HttpStatus httpStatus, @Nullable String description,
String scope) { @Nullable String errorUri, @Nullable String scope) {
super(errorCode, description, errorUri); super(errorCode, description, errorUri);
Assert.notNull(httpStatus, "httpStatus cannot be null"); Assert.notNull(httpStatus, "httpStatus cannot be null");
Assert.isTrue(isDescriptionValid(description), Assert.isTrue(isDescriptionValid(description),
@@ -85,13 +88,13 @@ public final class BearerTokenError extends OAuth2Error {
/** /**
* Return the scope. * 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; return this.scope;
} }
private static boolean isDescriptionValid(String description) { private static boolean isDescriptionValid(@Nullable String description) {
// @formatter:off // @formatter:off
return description == null || description.chars().allMatch((c) -> return description == null || description.chars().allMatch((c) ->
withinTheRangeOf(c, 0x20, 0x21) || withinTheRangeOf(c, 0x20, 0x21) ||
@@ -109,12 +112,12 @@ public final class BearerTokenError extends OAuth2Error {
// @formatter:on // @formatter:on
} }
private static boolean isErrorUriValid(String errorUri) { private static boolean isErrorUriValid(@Nullable String errorUri) {
return errorUri == null || errorUri.chars() return errorUri == null || errorUri.chars()
.allMatch((c) -> c == 0x21 || withinTheRangeOf(c, 0x23, 0x5B) || withinTheRangeOf(c, 0x5D, 0x7E)); .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 // @formatter:off
return scope == null || scope.chars().allMatch((c) -> return scope == null || scope.chars().allMatch((c) ->
withinTheRangeOf(c, 0x20, 0x21) || withinTheRangeOf(c, 0x20, 0x21) ||
@@ -16,6 +16,8 @@
package org.springframework.security.oauth2.server.resource; package org.springframework.security.oauth2.server.resource;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
/** /**
@@ -78,7 +80,7 @@ public final class BearerTokenErrors {
* @param scope the scope attribute to use in the error * @param scope the scope attribute to use in the error
* @return a {@link BearerTokenError} * @return a {@link BearerTokenError}
*/ */
public static BearerTokenError insufficientScope(String message, String scope) { public static BearerTokenError insufficientScope(String message, @Nullable String scope) {
try { try {
return new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN, message, return new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE, HttpStatus.FORBIDDEN, message,
DEFAULT_URI, scope); DEFAULT_URI, scope);
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource;
import java.io.Serial; import java.io.Serial;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 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 * {@link org.springframework.security.oauth2.core.OAuth2Error} instance as the
* {@code error_description}. * {@code error_description}.
* @param description the description * @param description the description
* @param cause the causing exception * @param cause the causing exception, or {@code null}
*/ */
public InvalidBearerTokenException(String description, Throwable cause) { public InvalidBearerTokenException(String description, @Nullable Throwable cause) {
super(BearerTokenErrors.invalidToken(description), cause); super(BearerTokenErrors.invalidToken(description));
if (cause != null) {
initCause(cause);
}
} }
} }
@@ -27,6 +27,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@@ -271,7 +273,10 @@ public final class OAuth2ProtectedResourceMetadata
valuesConsumer.accept(values); 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())) { if (URL.class.isAssignableFrom(url.getClass())) {
return; return;
} }
@@ -19,9 +19,13 @@ package org.springframework.security.oauth2.server.resource;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.ClaimAccessor; import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.util.Assert;
/** /**
* A {@link ClaimAccessor} for the claims a Resource Server describes about its * 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 * @return the {@code URL} the protected resource asserts as its resource identifier
*/ */
default URL getResource() { 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 * servers that can be used with this protected resource
* {@code (authorization_servers)}. * {@code (authorization_servers)}.
* @return a list of {@code issuer} identifier {@code URL}'s, for authorization * @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<URL> getAuthorizationServers() { default List<URL> getAuthorizationServers() {
List<String> authorizationServers = getClaimAsStringList( List<String> authorizationServers = getClaimAsStringList(
OAuth2ProtectedResourceMetadataClaimNames.AUTHORIZATION_SERVERS); OAuth2ProtectedResourceMetadataClaimNames.AUTHORIZATION_SERVERS);
if (authorizationServers == null) {
return Collections.emptyList();
}
List<URL> urls = new ArrayList<>(); List<URL> urls = new ArrayList<>();
authorizationServers.forEach((authorizationServer) -> { authorizationServers.forEach((authorizationServer) -> {
try { try {
@@ -70,11 +79,11 @@ public interface OAuth2ProtectedResourceMetadataClaimAccessor extends ClaimAcces
/** /**
* Returns a list of {@code scope} values supported, that are used in authorization * Returns a list of {@code scope} values supported, that are used in authorization
* requests to request access to this protected resource {@code (scopes_supported)}. * requests to request access to this protected resource {@code (scopes_supported)}.
* @return a list of {@code scope} values supported, that are used in authorization * @return a list of {@code scope} values supported, or an empty list if not set
* requests to request access to this protected resource
*/ */
default List<String> getScopes() { default List<String> getScopes() {
return getClaimAsStringList(OAuth2ProtectedResourceMetadataClaimNames.SCOPES_SUPPORTED); List<String> 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". * the protected resource. Defined values are "header", "body" and "query".
* {@code (bearer_methods_supported)}. * {@code (bearer_methods_supported)}.
* @return a list of the supported methods for sending an OAuth 2.0 bearer token to * @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<String> getBearerMethodsSupported() { default List<String> getBearerMethodsSupported() {
return getClaimAsStringList(OAuth2ProtectedResourceMetadataClaimNames.BEARER_METHODS_SUPPORTED); List<String> 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 * Returns the name of the protected resource intended for display to the end user
* {@code (resource_name)}. * {@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); return getClaimAsString(OAuth2ProtectedResourceMetadataClaimNames.RESOURCE_NAME);
} }
@@ -61,21 +61,20 @@ public abstract class AbstractOAuth2TokenAuthenticationToken<T extends OAuth2Tok
* Sub-class constructor. * Sub-class constructor.
*/ */
protected AbstractOAuth2TokenAuthenticationToken(T token) { protected AbstractOAuth2TokenAuthenticationToken(T token) {
this(token, null); this(token, null);
} }
/** /**
* Sub-class constructor. * Sub-class constructor.
* @param authorities the authorities assigned to the Access Token * @param authorities the authorities assigned to the Access Token, or {@code null}
*/ */
protected AbstractOAuth2TokenAuthenticationToken(T token, Collection<? extends GrantedAuthority> authorities) { protected AbstractOAuth2TokenAuthenticationToken(T token,
@Nullable Collection<? extends GrantedAuthority> authorities) {
this(token, token, token, authorities); this(token, token, token, authorities);
} }
protected AbstractOAuth2TokenAuthenticationToken(T token, Object principal, Object credentials, protected AbstractOAuth2TokenAuthenticationToken(T token, Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) { @Nullable Collection<? extends GrantedAuthority> authorities) {
super(authorities); super(authorities);
Assert.notNull(token, "token cannot be null"); Assert.notNull(token, "token cannot be null");
@@ -24,6 +24,7 @@ import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWK;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
@@ -72,13 +73,15 @@ public final class DPoPAuthenticationProvider implements AuthenticationProvider
public DPoPAuthenticationProvider(AuthenticationManager tokenAuthenticationManager) { public DPoPAuthenticationProvider(AuthenticationManager tokenAuthenticationManager) {
Assert.notNull(tokenAuthenticationManager, "tokenAuthenticationManager cannot be null"); Assert.notNull(tokenAuthenticationManager, "tokenAuthenticationManager cannot be null");
this.tokenAuthenticationManager = tokenAuthenticationManager; this.tokenAuthenticationManager = tokenAuthenticationManager;
Function<DPoPProofContext, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = ( Function<DPoPProofContext, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = (context) -> {
context) -> new DelegatingOAuth2TokenValidator<>( OAuth2AccessTokenClaims accessToken = context.getAccessToken();
// Use default validators Assert.notNull(accessToken, "accessToken cannot be null");
DPoPProofJwtDecoderFactory.DEFAULT_JWT_VALIDATOR_FACTORY.apply(context), return new DelegatingOAuth2TokenValidator<>(
// Add custom validators // Use default validators
new AthClaimValidator(context.getAccessToken()), DPoPProofJwtDecoderFactory.DEFAULT_JWT_VALIDATOR_FACTORY.apply(context),
new JwkThumbprintValidator(context.getAccessToken())); // Add custom validators
new AthClaimValidator(accessToken), new JwkThumbprintValidator(accessToken));
};
DPoPProofJwtDecoderFactory dPoPProofJwtDecoderFactory = new DPoPProofJwtDecoderFactory(); DPoPProofJwtDecoderFactory dPoPProofJwtDecoderFactory = new DPoPProofJwtDecoderFactory();
dPoPProofJwtDecoderFactory.setJwtValidatorFactory(jwtValidatorFactory); dPoPProofJwtDecoderFactory.setJwtValidatorFactory(jwtValidatorFactory);
this.dPoPProofVerifierFactory = dPoPProofJwtDecoderFactory; this.dPoPProofVerifierFactory = dPoPProofJwtDecoderFactory;
@@ -260,12 +263,12 @@ public final class DPoPAuthenticationProvider implements AuthenticationProvider
} }
@Override @Override
public Instant getIssuedAt() { public @Nullable Instant getIssuedAt() {
return this.accessToken.getIssuedAt(); return this.accessToken.getIssuedAt();
} }
@Override @Override
public Instant getExpiresAt() { public @Nullable Instant getExpiresAt() {
return this.accessToken.getExpiresAt(); return this.accessToken.getExpiresAt();
} }
@@ -114,7 +114,8 @@ public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthen
@Override @Override
public String getName() { public String getName() {
return getClaimAsString(this.principalClaimName); String name = this.getClaimAsString(this.principalClaimName);
return (name != null) ? name : "";
} }
} }
@@ -101,10 +101,12 @@ public final class JwtAuthenticationProvider implements AuthenticationProvider {
} }
catch (BadJwtException failed) { catch (BadJwtException failed) {
this.logger.debug("Failed to authenticate since the JWT was invalid"); this.logger.debug("Failed to authenticate since the JWT was invalid");
throw new InvalidBearerTokenException(failed.getMessage(), failed); throw new InvalidBearerTokenException((failed.getMessage() != null) ? failed.getMessage() : "Invalid token",
failed);
} }
catch (JwtException failed) { catch (JwtException failed) {
throw new AuthenticationServiceException(failed.getMessage(), failed); throw new AuthenticationServiceException(
(failed.getMessage() != null) ? failed.getMessage() : "Invalid token", failed);
} }
} }
@@ -43,7 +43,7 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
private static final long serialVersionUID = 620L; private static final long serialVersionUID = 620L;
private final String name; private final @Nullable String name;
/** /**
* Constructs a {@code JwtAuthenticationToken} using the provided parameters. * Constructs a {@code JwtAuthenticationToken} using the provided parameters.
@@ -92,8 +92,8 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
public JwtAuthenticationToken(Jwt jwt, Object principal, Collection<? extends GrantedAuthority> authorities) { public JwtAuthenticationToken(Jwt jwt, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(jwt, principal, jwt, authorities); super(jwt, principal, jwt, authorities);
this.setAuthenticated(true); this.setAuthenticated(true);
if (principal instanceof AuthenticatedPrincipal) { if (principal instanceof AuthenticatedPrincipal authenticatedPrincipal) {
this.name = ((AuthenticatedPrincipal) principal).getName(); this.name = authenticatedPrincipal.getName();
} }
else { else {
this.name = jwt.getSubject(); 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 @Override
public String getName() { public String getName() {
return this.name; return (this.name != null) ? this.name : "";
} }
@Override @Override
@@ -126,7 +127,7 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
*/ */
public static class Builder<B extends Builder<B>> extends AbstractOAuth2TokenAuthenticationBuilder<Jwt, B> { public static class Builder<B extends Builder<B>> extends AbstractOAuth2TokenAuthenticationBuilder<Jwt, B> {
private String name; private @Nullable String name;
protected Builder(JwtAuthenticationToken token) { protected Builder(JwtAuthenticationToken token) {
super(token); super(token);
@@ -168,10 +169,10 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
/** /**
* The name to use. * 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 * @return the {@link Builder} for further configurations
*/ */
public B name(String name) { public B name(@Nullable String name) {
this.name = name; this.name = name;
return (B) this; return (B) this;
} }
@@ -23,6 +23,7 @@ import java.util.Collections;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
@@ -105,7 +106,7 @@ public final class JwtGrantedAuthoritiesConverter implements Converter<Jwt, Coll
this.authoritiesClaimNames = Collections.singletonList(authoritiesClaimName); this.authoritiesClaimNames = Collections.singletonList(authoritiesClaimName);
} }
private String getAuthoritiesClaimName(Jwt jwt) { private @Nullable String getAuthoritiesClaimName(Jwt jwt) {
for (String claimName : this.authoritiesClaimNames) { for (String claimName : this.authoritiesClaimNames) {
if (jwt.hasClaim(claimName)) { if (jwt.hasClaim(claimName)) {
return claimName; return claimName;
@@ -29,7 +29,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -169,7 +168,7 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat
private static class JwtClaimIssuerConverter implements Converter<BearerTokenAuthenticationToken, String> { private static class JwtClaimIssuerConverter implements Converter<BearerTokenAuthenticationToken, String> {
@Override @Override
public String convert(@NonNull BearerTokenAuthenticationToken authentication) { public String convert(BearerTokenAuthenticationToken authentication) {
String token = authentication.getToken(); String token = authentication.getToken();
try { try {
String issuer = JWTParser.parse(token).getJWTClaimsSet().getIssuer(); String issuer = JWTParser.parse(token).getJWTClaimsSet().getIssuer();
@@ -178,7 +177,8 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat
} }
} }
catch (Exception cause) { 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); ex.setAuthenticationRequest(authentication);
throw ex; throw ex;
} }
@@ -202,6 +202,9 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat
} }
@Override @Override
@SuppressWarnings("NullAway") // Interface does not declare @Nullable; this
// implementation returns null when issuer not
// trusted
public AuthenticationManager resolve(String issuer) { public AuthenticationManager resolve(String issuer) {
if (this.trustedIssuer.test(issuer)) { if (this.trustedIssuer.test(issuer)) {
AuthenticationManager authenticationManager = this.authenticationManagers.computeIfAbsent(issuer, AuthenticationManager authenticationManager = this.authenticationManagers.computeIfAbsent(issuer,
@@ -31,7 +31,6 @@ import reactor.core.scheduler.Schedulers;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
@@ -172,7 +171,7 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver
private static class JwtClaimIssuerConverter implements Converter<BearerTokenAuthenticationToken, Mono<String>> { private static class JwtClaimIssuerConverter implements Converter<BearerTokenAuthenticationToken, Mono<String>> {
@Override @Override
public Mono<String> convert(@NonNull BearerTokenAuthenticationToken token) { public Mono<String> convert(BearerTokenAuthenticationToken token) {
try { try {
String issuer = JWTParser.parse(token.getToken()).getJWTClaimsSet().getIssuer(); String issuer = JWTParser.parse(token.getToken()).getJWTClaimsSet().getIssuer();
if (issuer == null) { if (issuer == null) {
@@ -184,7 +183,8 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver
} }
catch (Exception cause) { catch (Exception cause) {
return Mono.error(() -> { 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); ex.setAuthenticationRequest(token);
return ex; return ex;
}); });
@@ -76,7 +76,7 @@ public final class JwtReactiveAuthenticationManager implements ReactiveAuthentic
private AuthenticationException onError(JwtException ex) { private AuthenticationException onError(JwtException ex) {
if (ex instanceof BadJwtException) { 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); return new AuthenticationServiceException(ex.getMessage(), ex);
} }
@@ -22,6 +22,7 @@ import java.util.HashSet;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
@@ -104,7 +105,7 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
* @throws AuthenticationException if authentication failed for some reason * @throws AuthenticationException if authentication failed for some reason
*/ */
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public @Nullable Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!(authentication instanceof BearerTokenAuthenticationToken bearer)) { if (!(authentication instanceof BearerTokenAuthenticationToken bearer)) {
return null; return null;
} }
@@ -129,7 +130,8 @@ public final class OpaqueTokenAuthenticationProvider implements AuthenticationPr
} }
catch (BadOpaqueTokenException failed) { catch (BadOpaqueTokenException failed) {
this.logger.debug("Failed to authenticate since token was invalid"); 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) { catch (OAuth2IntrospectionException failed) {
throw new AuthenticationServiceException(failed.getMessage(), failed); throw new AuthenticationServiceException(failed.getMessage(), failed);
@@ -105,7 +105,7 @@ public class OpaqueTokenReactiveAuthenticationManager implements ReactiveAuthent
private AuthenticationException onError(OAuth2IntrospectionException ex) { private AuthenticationException onError(OAuth2IntrospectionException ex) {
if (ex instanceof BadOpaqueTokenException) { 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); return new AuthenticationServiceException(ex.getMessage(), ex);
} }
@@ -48,7 +48,8 @@ public final class ReactiveJwtAuthenticationConverter implements Converter<Jwt,
.collectList() .collectList()
.map((authorities) -> { .map((authorities) -> {
String principalName = jwt.getClaimAsString(this.principalClaimName); String principalName = jwt.getClaimAsString(this.principalClaimName);
return new JwtAuthenticationToken(jwt, authorities, principalName); return new JwtAuthenticationToken(jwt, authorities,
(principalName != null) ? principalName : "");
}); });
// @formatter:on // @formatter:on
} }
@@ -18,4 +18,7 @@
* OAuth 2.0 Resource Server {@code Authentication}s and supporting classes and * OAuth 2.0 Resource Server {@code Authentication}s and supporting classes and
* interfaces. * interfaces.
*/ */
@NullMarked
package org.springframework.security.oauth2.server.resource.authentication; package org.springframework.security.oauth2.server.resource.authentication;
import org.jspecify.annotations.NullMarked;
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.introspection;
import java.io.Serial; import java.io.Serial;
import org.jspecify.annotations.Nullable;
/** /**
* An exception similar to * An exception similar to
* {@link org.springframework.security.authentication.BadCredentialsException} that * {@link org.springframework.security.authentication.BadCredentialsException} that
@@ -35,7 +37,7 @@ public class BadOpaqueTokenException extends OAuth2IntrospectionException {
super(message); super(message);
} }
public BadOpaqueTokenException(String message, Throwable cause) { public BadOpaqueTokenException(String message, @Nullable Throwable cause) {
super(message, cause); super(message, cause);
} }
@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.introspection;
import java.io.Serial; import java.io.Serial;
import org.jspecify.annotations.Nullable;
/** /**
* Base exception for all OAuth 2.0 Introspection related errors * Base exception for all OAuth 2.0 Introspection related errors
* *
@@ -33,7 +35,7 @@ public class OAuth2IntrospectionException extends RuntimeException {
super(message); super(message);
} }
public OAuth2IntrospectionException(String message, Throwable cause) { public OAuth2IntrospectionException(String message, @Nullable Throwable cause) {
super(message, cause); super(message, cause);
} }
@@ -31,6 +31,7 @@ import java.util.function.Consumer;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
@@ -108,7 +109,7 @@ public final class RestClientOpaqueTokenIntrospector implements OpaqueTokenIntro
return spec.retrieve().toEntity(STRING_OBJECT_MAP); return spec.retrieve().toEntity(STRING_OBJECT_MAP);
} }
catch (Exception ex) { 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( private OAuth2IntrospectionAuthenticatedPrincipal defaultAuthenticationConverter(
OAuth2TokenIntrospectionClaimAccessor accessor) { OAuth2TokenIntrospectionClaimAccessor accessor) {
Collection<GrantedAuthority> authorities = authorities(accessor.getScopes()); List<String> scopes = accessor.getScopes();
Collection<GrantedAuthority> authorities = authorities((scopes != null) ? scopes : Collections.emptyList());
return new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities); return new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities);
} }
@@ -250,7 +252,7 @@ public final class RestClientOpaqueTokenIntrospector implements OpaqueTokenIntro
private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor { private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor {
@Override @Override
default List<String> getScopes() { default @Nullable List<String> getScopes() {
Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE); Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE);
if (value instanceof ArrayListFromString list) { if (value instanceof ArrayListFromString list) {
return list; return list;
@@ -270,9 +272,9 @@ public final class RestClientOpaqueTokenIntrospector implements OpaqueTokenIntro
private final String introspectionUri; private final String introspectionUri;
private String clientId; private @Nullable String clientId;
private String clientSecret; private @Nullable String clientSecret;
private final List<Consumer<RestClientOpaqueTokenIntrospector>> postProcessors = new ArrayList<>(); private final List<Consumer<RestClientOpaqueTokenIntrospector>> postProcessors = new ArrayList<>();
@@ -322,9 +324,13 @@ public final class RestClientOpaqueTokenIntrospector implements OpaqueTokenIntro
* @return the {@link RestClientOpaqueTokenIntrospector} * @return the {@link RestClientOpaqueTokenIntrospector}
*/ */
public RestClientOpaqueTokenIntrospector build() { public RestClientOpaqueTokenIntrospector build() {
RestClient restClient = RestClient.builder() RestClient.Builder builder = RestClient.builder();
.defaultHeaders((headers) -> headers.setBasicAuth(this.clientId, this.clientSecret)) if (this.clientId != null && this.clientSecret != null) {
.build(); String clientId = this.clientId;
String clientSecret = this.clientSecret;
builder.defaultHeaders((headers) -> headers.setBasicAuth(clientId, clientSecret));
}
RestClient restClient = builder.build();
RestClientOpaqueTokenIntrospector introspector = new RestClientOpaqueTokenIntrospector( RestClientOpaqueTokenIntrospector introspector = new RestClientOpaqueTokenIntrospector(
this.introspectionUri, restClient); this.introspectionUri, restClient);
this.postProcessors.forEach((postProcessor) -> postProcessor.accept(introspector)); this.postProcessors.forEach((postProcessor) -> postProcessor.accept(introspector));
@@ -32,6 +32,7 @@ import java.util.function.Consumer;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
@@ -157,7 +158,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
return this.restOperations.exchange(requestEntity, STRING_OBJECT_MAP); return this.restOperations.exchange(requestEntity, STRING_OBJECT_MAP);
} }
catch (Exception ex) { 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( private OAuth2IntrospectionAuthenticatedPrincipal defaultAuthenticationConverter(
OAuth2TokenIntrospectionClaimAccessor accessor) { OAuth2TokenIntrospectionClaimAccessor accessor) {
Collection<GrantedAuthority> authorities = authorities(accessor.getScopes()); List<String> scopes = accessor.getScopes();
Collection<GrantedAuthority> authorities = authorities((scopes != null) ? scopes : Collections.emptyList());
return new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities); return new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities);
} }
@@ -302,7 +304,7 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor { private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor {
@Override @Override
default List<String> getScopes() { default @Nullable List<String> getScopes() {
Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE); Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE);
if (value instanceof ArrayListFromString list) { if (value instanceof ArrayListFromString list) {
return list; return list;
@@ -322,9 +324,9 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final String introspectionUri; private final String introspectionUri;
private String clientId; private @Nullable String clientId;
private String clientSecret; private @Nullable String clientSecret;
private final List<Consumer<SpringOpaqueTokenIntrospector>> postProcessors = new ArrayList<>(); private final List<Consumer<SpringOpaqueTokenIntrospector>> postProcessors = new ArrayList<>();
@@ -379,7 +381,10 @@ public class SpringOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
*/ */
public SpringOpaqueTokenIntrospector build() { public SpringOpaqueTokenIntrospector build() {
RestTemplate restTemplate = new RestTemplate(); 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, SpringOpaqueTokenIntrospector introspector = new SpringOpaqueTokenIntrospector(this.introspectionUri,
restTemplate); restTemplate);
this.postProcessors.forEach((postProcessor) -> postProcessor.accept(introspector)); this.postProcessors.forEach((postProcessor) -> postProcessor.accept(introspector));
@@ -30,6 +30,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
@@ -188,7 +189,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
} }
private OAuth2IntrospectionException onError(Throwable ex) { 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<OAuth2IntrospectionAuthenticatedPrincipal> defaultAuthenticationConverter( private Mono<OAuth2IntrospectionAuthenticatedPrincipal> defaultAuthenticationConverter(
OAuth2TokenIntrospectionClaimAccessor accessor) { OAuth2TokenIntrospectionClaimAccessor accessor) {
Collection<GrantedAuthority> authorities = authorities(accessor.getScopes()); List<String> scopes = accessor.getScopes();
Collection<GrantedAuthority> authorities = authorities((scopes != null) ? scopes : Collections.emptyList());
return Mono.just(new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities)); return Mono.just(new OAuth2IntrospectionAuthenticatedPrincipal(accessor.getClaims(), authorities));
} }
@@ -255,7 +257,7 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor { private interface ArrayListFromStringClaimAccessor extends OAuth2TokenIntrospectionClaimAccessor {
@Override @Override
default List<String> getScopes() { default @Nullable List<String> getScopes() {
Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE); Object value = getClaims().get(OAuth2TokenIntrospectionClaimNames.SCOPE);
if (value instanceof ArrayListFromString list) { if (value instanceof ArrayListFromString list) {
return list; return list;
@@ -275,9 +277,9 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
private final String introspectionUri; private final String introspectionUri;
private String clientId; private @Nullable String clientId;
private String clientSecret; private @Nullable String clientSecret;
private final List<Consumer<SpringReactiveOpaqueTokenIntrospector>> postProcessors = new ArrayList<>(); private final List<Consumer<SpringReactiveOpaqueTokenIntrospector>> postProcessors = new ArrayList<>();
@@ -332,9 +334,13 @@ public class SpringReactiveOpaqueTokenIntrospector implements ReactiveOpaqueToke
* @since 6.5 * @since 6.5
*/ */
public SpringReactiveOpaqueTokenIntrospector build() { public SpringReactiveOpaqueTokenIntrospector build() {
WebClient webClient = WebClient.builder() WebClient.Builder builder = WebClient.builder();
.defaultHeaders((h) -> h.setBasicAuth(this.clientId, this.clientSecret)) if (this.clientId != null && this.clientSecret != null) {
.build(); String clientId = this.clientId;
String clientSecret = this.clientSecret;
builder.defaultHeaders((h) -> h.setBasicAuth(clientId, clientSecret));
}
WebClient webClient = builder.build();
SpringReactiveOpaqueTokenIntrospector introspector = new SpringReactiveOpaqueTokenIntrospector( SpringReactiveOpaqueTokenIntrospector introspector = new SpringReactiveOpaqueTokenIntrospector(
this.introspectionUri, webClient); this.introspectionUri, webClient);
this.postProcessors.forEach((postProcessor) -> postProcessor.accept(introspector)); this.postProcessors.forEach((postProcessor) -> postProcessor.accept(introspector));
@@ -17,4 +17,7 @@
/** /**
* OAuth 2.0 Introspection supporting classes and interfaces. * OAuth 2.0 Introspection supporting classes and interfaces.
*/ */
@NullMarked
package org.springframework.security.oauth2.server.resource.introspection; package org.springframework.security.oauth2.server.resource.introspection;
import org.jspecify.annotations.NullMarked;
@@ -17,4 +17,7 @@
/** /**
* OAuth 2.0 Resource Server core classes and interfaces providing support. * OAuth 2.0 Resource Server core classes and interfaces providing support.
*/ */
@NullMarked
package org.springframework.security.oauth2.server.resource; package org.springframework.security.oauth2.server.resource;
import org.jspecify.annotations.NullMarked;
@@ -22,6 +22,7 @@ import java.util.function.Function;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -51,7 +52,7 @@ import org.springframework.web.util.UriComponentsBuilder;
*/ */
public final class BearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint { public final class BearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint {
private String realmName; private @Nullable String realmName;
private Function<HttpServletRequest, String> resourceMetadataParameterResolver = BearerTokenAuthenticationEntryPoint::getResourceMetadataParameter; private Function<HttpServletRequest, String> 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 * 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; this.realmName = realmName;
} }
@@ -17,6 +17,7 @@
package org.springframework.security.oauth2.server.resource.web; package org.springframework.security.oauth2.server.resource.web;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 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 * @return the Bearer Token value or {@code null} if none found
* @throws OAuth2AuthenticationException if the found token is invalid * @throws OAuth2AuthenticationException if the found token is invalid
*/ */
String resolve(HttpServletRequest request); @Nullable String resolve(HttpServletRequest request);
} }
@@ -20,6 +20,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@@ -51,7 +52,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION; private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
@Override @Override
public String resolve(final HttpServletRequest request) { public @Nullable String resolve(final HttpServletRequest request) {
// @formatter:off // @formatter:off
return resolveToken( return resolveToken(
resolveFromAuthorizationHeader(request), resolveFromAuthorizationHeader(request),
@@ -61,7 +62,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
// @formatter:on // @formatter:on
} }
private static String resolveToken(String... accessTokens) { private static @Nullable String resolveToken(@Nullable String... accessTokens) {
if (accessTokens == null || accessTokens.length == 0) { if (accessTokens == null || accessTokens.length == 0) {
return null; return null;
} }
@@ -87,7 +88,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
return accessToken; return accessToken;
} }
private String resolveFromAuthorizationHeader(HttpServletRequest request) { private @Nullable String resolveFromAuthorizationHeader(HttpServletRequest request) {
String authorization = request.getHeader(this.bearerTokenHeaderName); String authorization = request.getHeader(this.bearerTokenHeaderName);
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) { if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
return null; return null;
@@ -102,7 +103,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
return matcher.group("token"); return matcher.group("token");
} }
private String resolveAccessTokenFromQueryString(HttpServletRequest request) { private @Nullable String resolveAccessTokenFromQueryString(HttpServletRequest request) {
if (!this.allowUriQueryParameter || !HttpMethod.GET.name().equals(request.getMethod())) { if (!this.allowUriQueryParameter || !HttpMethod.GET.name().equals(request.getMethod())) {
return null; return null;
} }
@@ -110,7 +111,7 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver {
return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME)); return resolveToken(request.getParameterValues(ACCESS_TOKEN_PARAMETER_NAME));
} }
private String resolveAccessTokenFromBody(HttpServletRequest request) { private @Nullable String resolveAccessTokenFromBody(HttpServletRequest request) {
if (!this.allowFormEncodedBodyParameter if (!this.allowFormEncodedBodyParameter
|| !MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()) || !MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())
|| HttpMethod.GET.name().equals(request.getMethod())) { || HttpMethod.GET.name().equals(request.getMethod())) {
@@ -24,6 +24,7 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@@ -60,7 +61,7 @@ public final class OAuth2ProtectedResourceMetadataFilter extends OncePerRequestF
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() { private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
}; };
private static final GenericHttpMessageConverter<Object> JSON_MESSAGE_CONVERTER = HttpMessageConverters private static final @Nullable GenericHttpMessageConverter<Object> JSON_MESSAGE_CONVERTER = HttpMessageConverters
.getJsonMessageConverter(); .getJsonMessageConverter();
/** /**
@@ -108,8 +109,10 @@ public final class OAuth2ProtectedResourceMetadataFilter extends OncePerRequestF
OAuth2ProtectedResourceMetadata protectedResourceMetadata = builder.build(); OAuth2ProtectedResourceMetadata protectedResourceMetadata = builder.build();
try { try {
GenericHttpMessageConverter<Object> converter = JSON_MESSAGE_CONVERTER;
Assert.notNull(converter, "No JSON message converter available");
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); 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); MediaType.APPLICATION_JSON, httpResponse);
} }
catch (Exception ex) { catch (Exception ex) {
@@ -161,7 +164,7 @@ public final class OAuth2ProtectedResourceMetadataFilter extends OncePerRequestF
} }
@SuppressWarnings("removal") @SuppressWarnings("removal")
private static GenericHttpMessageConverter<Object> getJsonMessageConverter() { private static @Nullable GenericHttpMessageConverter<Object> getJsonMessageConverter() {
if (jacksonPresent) { if (jacksonPresent) {
return new GenericHttpMessageConverterAdapter<>(new JacksonJsonHttpMessageConverter()); return new GenericHttpMessageConverterAdapter<>(new JacksonJsonHttpMessageConverter());
} }
@@ -21,6 +21,7 @@ import java.util.Map;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -46,7 +47,7 @@ import org.springframework.security.web.access.AccessDeniedHandler;
*/ */
public final class BearerTokenAccessDeniedHandler implements 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 * 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 * Set the default realm name to use in the bearer token error response
* @param realmName * @param realmName
*/ */
public void setRealmName(String realmName) { public void setRealmName(@Nullable String realmName) {
this.realmName = realmName; this.realmName = realmName;
} }
@@ -17,4 +17,7 @@
/** /**
* OAuth 2.0 Resource Server access denial classes and interfaces. * OAuth 2.0 Resource Server access denial classes and interfaces.
*/ */
@NullMarked
package org.springframework.security.oauth2.server.resource.web.access; package org.springframework.security.oauth2.server.resource.web.access;
import org.jspecify.annotations.NullMarked;
@@ -21,6 +21,7 @@ import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -51,7 +52,7 @@ public class BearerTokenServerAccessDeniedHandler implements ServerAccessDeniedH
private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = Arrays.asList("scope", "scp"); private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES = Arrays.asList("scope", "scp");
private String realmName; private @Nullable String realmName;
@Override @Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) { public Mono<Void> 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 * Set the default realm name to use in the bearer token error response
* @param realmName * @param realmName
*/ */
public final void setRealmName(String realmName) { public final void setRealmName(@Nullable String realmName) {
this.realmName = realmName; this.realmName = realmName;
} }
@@ -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;
@@ -17,6 +17,7 @@
package org.springframework.security.oauth2.server.resource.web.authentication; package org.springframework.security.oauth2.server.resource.web.authentication;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -43,7 +44,7 @@ public final class BearerTokenAuthenticationConverter implements AuthenticationC
private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
@Override @Override
public Authentication convert(HttpServletRequest request) { public @Nullable Authentication convert(HttpServletRequest request) {
String token = this.bearerTokenResolver.resolve(request); String token = this.bearerTokenResolver.resolve(request);
if (StringUtils.hasText(token)) { if (StringUtils.hasText(token)) {
BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(token); BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(token);
@@ -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;
@@ -17,4 +17,7 @@
/** /**
* OAuth 2.0 Resource Server {@code Filter}'s and supporting classes and interfaces. * OAuth 2.0 Resource Server {@code Filter}'s and supporting classes and interfaces.
*/ */
@NullMarked
package org.springframework.security.oauth2.server.resource.web; package org.springframework.security.oauth2.server.resource.web;
import org.jspecify.annotations.NullMarked;
@@ -20,8 +20,8 @@ import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.core.OAuth2Token; 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.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
@@ -63,17 +63,18 @@ public final class ServerBearerExchangeFilterFunction implements ExchangeFilterF
private Mono<OAuth2Token> oauth2Token() { private Mono<OAuth2Token> oauth2Token() {
// @formatter:off // @formatter:off
return currentAuthentication() return currentAuthentication()
.filter((authentication) -> authentication.getCredentials() instanceof OAuth2Token) .filter((authentication) -> authentication.getCredentials() != null
.map(Authentication::getCredentials) && authentication.getCredentials() instanceof OAuth2Token)
.cast(OAuth2Token.class); .map((authentication) -> {
Object credentials = authentication.getCredentials();
Assert.notNull(credentials, "credentials cannot be null");
return (OAuth2Token) credentials;
});
// @formatter:on // @formatter:on
} }
private Mono<Authentication> currentAuthentication() { private Mono<Authentication> currentAuthentication() {
// @formatter:off return ReactiveSecurityContextHolder.getContext().flatMap((ctx) -> Mono.justOrEmpty(ctx.getAuthentication()));
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication);
// @formatter:on
} }
private ClientRequest bearer(ClientRequest request, OAuth2Token token) { private ClientRequest bearer(ClientRequest request, OAuth2Token token) {
@@ -18,11 +18,13 @@ package org.springframework.security.oauth2.server.resource.web.reactive.functio
import java.util.Map; import java.util.Map;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.util.context.Context; import reactor.util.context.Context;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2Token; 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.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
@@ -71,9 +73,13 @@ public final class ServletBearerExchangeFilterFunction implements ExchangeFilter
return Mono.deferContextual(Mono::just) return Mono.deferContextual(Mono::just)
.cast(Context.class) .cast(Context.class)
.flatMap(this::currentAuthentication) .flatMap(this::currentAuthentication)
.filter((authentication) -> authentication.getCredentials() instanceof OAuth2Token) .filter((authentication) -> authentication.getCredentials() != null
.map(Authentication::getCredentials) && authentication.getCredentials() instanceof OAuth2Token)
.cast(OAuth2Token.class); .map((authentication) -> {
Object credentials = authentication.getCredentials();
Assert.notNull(credentials, "credentials cannot be null");
return (OAuth2Token) credentials;
});
// @formatter:on // @formatter:on
} }
@@ -81,7 +87,7 @@ public final class ServletBearerExchangeFilterFunction implements ExchangeFilter
return Mono.justOrEmpty(getAttribute(ctx, Authentication.class)); return Mono.justOrEmpty(getAttribute(ctx, Authentication.class));
} }
private <T> T getAttribute(Context ctx, Class<T> clazz) { private <T> @Nullable T getAttribute(Context ctx, Class<T> clazz) {
// NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds // NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds
// this key // this key
if (!ctx.hasKey(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY)) { if (!ctx.hasKey(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY)) {
@@ -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;
@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.server.resource.web.server;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -49,9 +50,9 @@ import org.springframework.web.server.ServerWebExchange;
*/ */
public final class BearerTokenServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint { 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; this.realmName = realmName;
} }
@@ -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;
@@ -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;
@@ -47,10 +47,10 @@ public class JwtAuthenticationTokenTests {
} }
@Test @Test
public void getNameWhenJwtHasNoSubjectThenReturnsNull() { public void getNameWhenJwtHasNoSubjectThenReturnsEmptyString() {
Jwt jwt = builder().claim("claim", "value").build(); Jwt jwt = builder().claim("claim", "value").build();
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt); JwtAuthenticationToken token = new JwtAuthenticationToken(jwt);
assertThat(token.getName()).isNull(); assertThat(token.getName()).isEmpty();
} }
@Test @Test
@@ -108,12 +108,12 @@ public class JwtAuthenticationTokenTests {
} }
@Test @Test
public void getNameWhenConstructedWithNoSubjectThenReturnsNull() { public void getNameWhenConstructedWithNoSubjectThenReturnsEmptyString() {
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("test"); Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("test");
Jwt jwt = builder().claim("claim", "value").build(); Jwt jwt = builder().claim("claim", "value").build();
assertThat(new JwtAuthenticationToken(jwt, authorities, (String) null).getName()).isNull(); assertThat(new JwtAuthenticationToken(jwt, authorities, (String) null).getName()).isEmpty();
assertThat(new JwtAuthenticationToken(jwt, authorities).getName()).isNull(); assertThat(new JwtAuthenticationToken(jwt, authorities).getName()).isEmpty();
assertThat(new JwtAuthenticationToken(jwt).getName()).isNull(); assertThat(new JwtAuthenticationToken(jwt).getName()).isEmpty();
} }
@Test @Test