Enable null-safety in spring-security-oauth2-jose

Closes gh-17821
This commit is contained in:
Joe Grandja 2026-03-12 13:51:49 -04:00
parent 78f762fab8
commit 22a98583f1
25 changed files with 178 additions and 102 deletions

View File

@ -147,7 +147,7 @@ class OAuth2ResourceServerDslTests {
}
class MockJwtDecoder: JwtDecoder {
override fun decode(token: String?): Jwt {
override fun decode(token: String): Jwt {
return Jwt.withTokenValue("token")
.header("alg", "none")
.claim(SUB, "user")

View File

@ -207,7 +207,7 @@ class JwtDslTests {
}
class MockJwtDecoder: JwtDecoder {
override fun decode(token: String?): Jwt {
override fun decode(token: String): Jwt {
return Jwt.withTokenValue("some tokenValue").build()
}
}

View File

@ -168,7 +168,7 @@ class ServerJwtDslTests {
}
class NullReactiveJwtDecoder: ReactiveJwtDecoder {
override fun decode(token: String?): Mono<Jwt> {
override fun decode(token: String): Mono<Jwt> {
return Mono.empty()
}
}

View File

@ -29,7 +29,8 @@ import org.springframework.stereotype.Component
class UserDetailsJwtPrincipalConverter(private val users: UserDetailsService) : Converter<Jwt, OAuth2AuthenticatedPrincipal> {
override fun convert(jwt: Jwt): OAuth2AuthenticatedPrincipal {
val user = users.loadUserByUsername(jwt.subject)
val subject = jwt.subject ?: throw IllegalArgumentException("JWT subject is required")
val user = users.loadUserByUsername(subject)
return JwtUser(jwt, user)
}
@ -37,7 +38,7 @@ class UserDetailsJwtPrincipalConverter(private val users: UserDetailsService) :
User(user.username, user.password, user.isEnabled, user.isAccountNonExpired, user.isCredentialsNonExpired, user.isAccountNonLocked, user.authorities),
OAuth2AuthenticatedPrincipal {
override fun getName(): String = jwt.subject
override fun getName(): String = jwt.subject ?: ""
override fun getAttributes(): Map<String, Any> = jwt.claims

View File

@ -1,5 +1,9 @@
plugins {
id 'javadoc-warnings-error'
id 'security-nullability'
}
apply plugin: 'io.spring.convention.spring-module'
apply plugin: 'javadoc-warnings-error'
dependencies {
management platform(project(":spring-security-dependencies"))

View File

@ -16,6 +16,8 @@
package org.springframework.security.oauth2.jose.jws;
import org.jspecify.annotations.Nullable;
/**
* An enumeration of the cryptographic algorithms defined by the JSON Web Algorithms (JWA)
* specification and used by JSON Web Signature (JWS) to create a MAC of the contents of
@ -69,7 +71,7 @@ public enum MacAlgorithm implements JwsAlgorithm {
* @param name the algorithm name
* @return the resolved {@code MacAlgorithm}, or {@code null} if not found
*/
public static MacAlgorithm from(String name) {
public static @Nullable MacAlgorithm from(String name) {
for (MacAlgorithm algorithm : values()) {
if (algorithm.getName().equals(name)) {
return algorithm;

View File

@ -16,6 +16,8 @@
package org.springframework.security.oauth2.jose.jws;
import org.jspecify.annotations.Nullable;
/**
* An enumeration of the cryptographic algorithms defined by the JSON Web Algorithms (JWA)
* specification and used by JSON Web Signature (JWS) to digitally sign the contents of
@ -99,7 +101,7 @@ public enum SignatureAlgorithm implements JwsAlgorithm {
* @param name the algorithm name
* @return the resolved {@code SignatureAlgorithm}, or {@code null} if not found
*/
public static SignatureAlgorithm from(String name) {
public static @Nullable SignatureAlgorithm from(String name) {
for (SignatureAlgorithm value : values()) {
if (value.getName().equals(name)) {
return value;

View File

@ -17,4 +17,7 @@
/**
* Core classes and interfaces providing support for JSON Web Signature (JWS).
*/
@NullMarked
package org.springframework.security.oauth2.jose.jws;
import org.jspecify.annotations.NullMarked;

View File

@ -0,0 +1,24 @@
/*
* 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.
*/
/**
* Core classes and interfaces providing support for JavaScript Object Signing and
* Encryption (JOSE).
*/
@NullMarked
package org.springframework.security.oauth2.jose;
import org.jspecify.annotations.NullMarked;

View File

@ -18,7 +18,8 @@ package org.springframework.security.oauth2.jwt;
import java.net.URI;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.util.Assert;
@ -38,7 +39,7 @@ public final class DPoPProofContext {
private final String targetUri;
private final OAuth2Token accessToken;
private final @Nullable OAuth2Token accessToken;
private DPoPProofContext(String dPoPProof, String method, String targetUri, @Nullable OAuth2Token accessToken) {
this.dPoPProof = dPoPProof;
@ -82,8 +83,7 @@ public final class DPoPProofContext {
* {@code null}
*/
@SuppressWarnings("unchecked")
@Nullable
public <T extends OAuth2Token> T getAccessToken() {
public @Nullable <T extends OAuth2Token> T getAccessToken() {
return (T) this.accessToken;
}
@ -103,11 +103,11 @@ public final class DPoPProofContext {
private String dPoPProof;
private String method;
private @Nullable String method;
private String targetUri;
private @Nullable String targetUri;
private OAuth2Token accessToken;
private @Nullable OAuth2Token accessToken;
private Builder(String dPoPProof) {
Assert.hasText(dPoPProof, "dPoPProof cannot be empty");
@ -144,7 +144,7 @@ public final class DPoPProofContext {
* request
* @return the {@link Builder}
*/
public Builder accessToken(OAuth2Token accessToken) {
public Builder accessToken(@Nullable OAuth2Token accessToken) {
this.accessToken = accessToken;
return this;
}
@ -154,13 +154,13 @@ public final class DPoPProofContext {
* @return a {@link DPoPProofContext}
*/
public DPoPProofContext build() {
Assert.hasText(this.method, "method cannot be empty");
Assert.hasText(this.targetUri, "targetUri cannot be empty");
validate();
return new DPoPProofContext(this.dPoPProof, this.method, this.targetUri, this.accessToken);
}
private void validate() {
Assert.hasText(this.method, "method cannot be empty");
Assert.hasText(this.targetUri, "targetUri cannot be empty");
if (!"GET".equals(this.method) && !"HEAD".equals(this.method) && !"POST".equals(this.method)
&& !"PUT".equals(this.method) && !"PATCH".equals(this.method) && !"DELETE".equals(this.method)
&& !"OPTIONS".equals(this.method) && !"TRACE".equals(this.method)) {

View File

@ -25,6 +25,8 @@ import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.converter.ClaimConversionService;
import org.springframework.security.oauth2.jose.JwaAlgorithm;
import org.springframework.util.Assert;
@ -59,36 +61,37 @@ class JoseHeader {
* encrypt the JWE.
* @return the {@link JwaAlgorithm}
*/
@SuppressWarnings("unchecked")
public <T extends JwaAlgorithm> T getAlgorithm() {
return (T) getHeader(JoseHeaderNames.ALG);
T algorithm = getHeader(JoseHeaderNames.ALG);
Assert.notNull(algorithm, "algorithm cannot be null");
return algorithm;
}
/**
* Returns the JWK Set URL that refers to the resource of a set of JSON-encoded public
* keys, one of which corresponds to the key used to digitally sign the JWS or encrypt
* the JWE.
* @return the JWK Set URL
* @return the JWK Set URL, or {@code null} if the header is absent
*/
public URL getJwkSetUrl() {
public @Nullable URL getJwkSetUrl() {
return getHeader(JoseHeaderNames.JKU);
}
/**
* Returns the JSON Web Key which is the public key that corresponds to the key used
* to digitally sign the JWS or encrypt the JWE.
* @return the JSON Web Key
* @return the JSON Web Key, or {@code null} if the header is absent
*/
public Map<String, Object> getJwk() {
public @Nullable Map<String, Object> getJwk() {
return getHeader(JoseHeaderNames.JWK);
}
/**
* Returns the key ID that is a hint indicating which key was used to secure the JWS
* or JWE.
* @return the key ID
* @return the key ID, or {@code null} if the header is absent
*/
public String getKeyId() {
public @Nullable String getKeyId() {
return getHeader(JoseHeaderNames.KID);
}
@ -96,9 +99,9 @@ class JoseHeader {
* Returns the X.509 URL that refers to the resource for the X.509 public key
* certificate or certificate chain corresponding to the key used to digitally sign
* the JWS or encrypt the JWE.
* @return the X.509 URL
* @return the X.509 URL, or {@code null} if the header is absent
*/
public URL getX509Url() {
public @Nullable URL getX509Url() {
return getHeader(JoseHeaderNames.X5U);
}
@ -108,9 +111,9 @@ class JoseHeader {
* encrypt the JWE. The certificate or certificate chain is represented as a
* {@code List} of certificate value {@code String}s. Each {@code String} in the
* {@code List} is a Base64-encoded DER PKIX certificate value.
* @return the X.509 certificate chain
* @return the X.509 certificate chain, or {@code null} if the header is absent
*/
public List<String> getX509CertificateChain() {
public @Nullable List<String> getX509CertificateChain() {
return getHeader(JoseHeaderNames.X5C);
}
@ -118,7 +121,8 @@ class JoseHeader {
* Returns the X.509 certificate SHA-1 thumbprint that is a base64url-encoded SHA-1
* thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
* @return the X.509 certificate SHA-1 thumbprint
* @return the X.509 certificate SHA-1 thumbprint, or {@code null} if the header is
* absent
* @deprecated The SHA-1 algorithm has been proven to be vulnerable to collision
* attacks and should not be used. See the <a target="_blank" href=
* "https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html">Google
@ -128,7 +132,7 @@ class JoseHeader {
* the first SHA1 collision</a>
*/
@Deprecated
public String getX509SHA1Thumbprint() {
public @Nullable String getX509SHA1Thumbprint() {
return getHeader(JoseHeaderNames.X5T);
}
@ -136,35 +140,36 @@ class JoseHeader {
* Returns the X.509 certificate SHA-256 thumbprint that is a base64url-encoded
* SHA-256 thumbprint (a.k.a. digest) of the DER encoding of the X.509 certificate
* corresponding to the key used to digitally sign the JWS or encrypt the JWE.
* @return the X.509 certificate SHA-256 thumbprint
* @return the X.509 certificate SHA-256 thumbprint, or {@code null} if the header is
* absent
*/
public String getX509SHA256Thumbprint() {
public @Nullable String getX509SHA256Thumbprint() {
return getHeader(JoseHeaderNames.X5T_S256);
}
/**
* Returns the type header that declares the media type of the JWS/JWE.
* @return the type header
* @return the type header, or {@code null} if the header is absent
*/
public String getType() {
public @Nullable String getType() {
return getHeader(JoseHeaderNames.TYP);
}
/**
* Returns the content type header that declares the media type of the secured content
* (the payload).
* @return the content type header
* @return the content type header, or {@code null} if the header is absent
*/
public String getContentType() {
public @Nullable String getContentType() {
return getHeader(JoseHeaderNames.CTY);
}
/**
* Returns the critical headers that indicates which extensions to the JWS/JWE/JWA
* specifications are being used that MUST be understood and processed.
* @return the critical headers
* @return the critical headers, or {@code null} if the header is absent
*/
public Set<String> getCritical() {
public @Nullable Set<String> getCritical() {
return getHeader(JoseHeaderNames.CRIT);
}
@ -180,10 +185,10 @@ class JoseHeader {
* Returns the header value.
* @param name the header name
* @param <T> the type of the header value
* @return the header value
* @return the header value, or {@code null} if the header is absent
*/
@SuppressWarnings("unchecked")
public <T> T getHeader(String name) {
public <T> @Nullable T getHeader(String name) {
Assert.hasText(name, "name cannot be empty");
return (T) getHeaders().get(name);
}

View File

@ -24,6 +24,8 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.util.Assert;
@ -60,13 +62,14 @@ public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
/**
* Constructs a {@code Jwt} using the provided parameters.
* @param tokenValue the token value
* @param issuedAt the time at which the JWT was issued
* @param expiresAt the expiration time on or after which the JWT MUST NOT be accepted
* @param issuedAt the time at which the JWT was issued, may be {@code null}
* @param expiresAt the expiration time on or after which the JWT MUST NOT be
* accepted, may be {@code null}
* @param headers the JOSE header(s)
* @param claims the JWT Claims Set
*
*/
public Jwt(String tokenValue, Instant issuedAt, Instant expiresAt, Map<String, Object> headers,
public Jwt(String tokenValue, @Nullable Instant issuedAt, @Nullable Instant expiresAt, Map<String, Object> headers,
Map<String, Object> claims) {
super(tokenValue, issuedAt, expiresAt);
Assert.notEmpty(headers, "headers cannot be empty");
@ -252,7 +255,7 @@ public class Jwt extends AbstractOAuth2Token implements JwtClaimAccessor {
return new Jwt(this.tokenValue, iat, exp, this.headers, this.claims);
}
private Instant toInstant(Object timestamp) {
private @Nullable Instant toInstant(@Nullable Object timestamp) {
if (timestamp != null) {
Assert.isInstanceOf(Instant.class, timestamp, "timestamps must be of type Instant");
}

View File

@ -20,6 +20,8 @@ import java.net.URL;
import java.time.Instant;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.ClaimAccessor;
/**
@ -39,27 +41,28 @@ public interface JwtClaimAccessor extends ClaimAccessor {
/**
* Returns the Issuer {@code (iss)} claim which identifies the principal that issued
* the JWT.
* @return the Issuer identifier
* @return the Issuer identifier, or {@code null} if the claim is missing
*/
default URL getIssuer() {
default @Nullable URL getIssuer() {
return this.getClaimAsURL(JwtClaimNames.ISS);
}
/**
* Returns the Subject {@code (sub)} claim which identifies the principal that is the
* subject of the JWT.
* @return the Subject identifier
* @return the Subject identifier, or {@code null} if the claim is missing
*/
default String getSubject() {
default @Nullable String getSubject() {
return this.getClaimAsString(JwtClaimNames.SUB);
}
/**
* Returns the Audience {@code (aud)} claim which identifies the recipient(s) that the
* JWT is intended for.
* @return the Audience(s) that this JWT intended for
* @return the Audience(s) that this JWT intended for, or {@code null} if the claim is
* missing
*/
default List<String> getAudience() {
default @Nullable List<String> getAudience() {
return this.getClaimAsStringList(JwtClaimNames.AUD);
}
@ -67,9 +70,9 @@ public interface JwtClaimAccessor extends ClaimAccessor {
* Returns the Expiration time {@code (exp)} claim which identifies the expiration
* time on or after which the JWT MUST NOT be accepted for processing.
* @return the Expiration time on or after which the JWT MUST NOT be accepted for
* processing
* processing, or {@code null} if the claim is missing
*/
default Instant getExpiresAt() {
default @Nullable Instant getExpiresAt() {
return this.getClaimAsInstant(JwtClaimNames.EXP);
}
@ -77,27 +80,29 @@ public interface JwtClaimAccessor extends ClaimAccessor {
* Returns the Not Before {@code (nbf)} claim which identifies the time before which
* the JWT MUST NOT be accepted for processing.
* @return the Not Before time before which the JWT MUST NOT be accepted for
* processing
* processing, or {@code null} if the claim is missing
*/
default Instant getNotBefore() {
default @Nullable Instant getNotBefore() {
return this.getClaimAsInstant(JwtClaimNames.NBF);
}
/**
* Returns the Issued at {@code (iat)} claim which identifies the time at which the
* JWT was issued.
* @return the Issued at claim which identifies the time at which the JWT was issued
* @return the Issued at claim which identifies the time at which the JWT was issued,
* or {@code null} if the claim is missing
*/
default Instant getIssuedAt() {
default @Nullable Instant getIssuedAt() {
return this.getClaimAsInstant(JwtClaimNames.IAT);
}
/**
* Returns the JWT ID {@code (jti)} claim which provides a unique identifier for the
* JWT.
* @return the JWT ID claim which provides a unique identifier for the JWT
* @return the JWT ID claim which provides a unique identifier for the JWT, or
* {@code null} if the claim is missing
*/
default String getId() {
default @Nullable String getId() {
return this.getClaimAsString(JwtClaimNames.JTI);
}

View File

@ -62,8 +62,10 @@ public final class JwtClaimValidator<T> implements OAuth2TokenValidator<Jwt> {
public OAuth2TokenValidatorResult validate(Jwt token) {
Assert.notNull(token, "token cannot be null");
T claimValue = token.getClaim(this.claim);
if (this.test.test(claimValue)) {
return OAuth2TokenValidatorResult.success();
if (claimValue != null) {
if (this.test.test(claimValue)) {
return OAuth2TokenValidatorResult.success();
}
}
this.logger.debug(this.error.getDescription());
return OAuth2TokenValidatorResult.failure(this.error);

View File

@ -164,6 +164,7 @@ final class JwtDecoderProviderConfigurationUtils {
RequestEntity<Void> request = RequestEntity.get(uri.toUriString()).build();
ResponseEntity<Map<String, Object>> response = rest.exchange(request, STRING_OBJECT_MAP);
Map<String, Object> configuration = response.getBody();
Assert.notNull(configuration, "configuration must not be null");
Assert.isTrue(configuration.get("jwks_uri") != null, "The public JWK set URI must not be null");
return configuration;
}

View File

@ -109,7 +109,9 @@ public final class JwtDecoders {
private static JwtDecoder withProviderConfiguration(Map<String, Object> configuration, String issuer) {
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
String jwkSetUri = configuration.get("jwks_uri").toString();
Object jwksUri = configuration.get("jwks_uri");
Assert.notNull(jwksUri, "The public JWK Set URI must not be null");
String jwkSetUri = jwksUri.toString();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri)
.jwtProcessorCustomizer(JwtDecoderProviderConfigurationUtils::addJWSAlgorithms)
.build();

View File

@ -16,7 +16,8 @@
package org.springframework.security.oauth2.jwt;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@ -30,11 +31,11 @@ import org.springframework.util.Assert;
*/
public final class JwtEncoderParameters {
private final JwsHeader jwsHeader;
private final @Nullable JwsHeader jwsHeader;
private final JwtClaimsSet claims;
private JwtEncoderParameters(JwsHeader jwsHeader, JwtClaimsSet claims) {
private JwtEncoderParameters(@Nullable JwsHeader jwsHeader, JwtClaimsSet claims) {
this.jwsHeader = jwsHeader;
this.claims = claims;
}
@ -67,8 +68,7 @@ public final class JwtEncoderParameters {
* Returns the {@link JwsHeader JWS headers}.
* @return the {@link JwsHeader}, or {@code null} if not specified
*/
@Nullable
public JwsHeader getJwsHeader() {
public @Nullable JwsHeader getJwsHeader() {
return this.jwsHeader;
}

View File

@ -23,6 +23,8 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
@ -50,7 +52,7 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
private final Map<String, Converter<Object, ?>> claimTypeConverters;
private final Map<String, Converter<Object, ? extends @Nullable Object>> claimTypeConverters;
/**
* Constructs a {@link MappedJwtClaimSetConverter} with the provided arguments
@ -62,7 +64,7 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
* claim set.
* @param claimTypeConverters The {@link Map} of converters to use
*/
public MappedJwtClaimSetConverter(Map<String, Converter<Object, ?>> claimTypeConverters) {
public MappedJwtClaimSetConverter(Map<String, Converter<Object, ? extends @Nullable Object>> claimTypeConverters) {
Assert.notNull(claimTypeConverters, "claimTypeConverters cannot be null");
this.claimTypeConverters = claimTypeConverters;
}
@ -96,12 +98,13 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
* @return An instance of {@link MappedJwtClaimSetConverter} that contains the
* converters provided, plus any defaults that were not overridden.
*/
public static MappedJwtClaimSetConverter withDefaults(Map<String, Converter<Object, ?>> claimTypeConverters) {
public static MappedJwtClaimSetConverter withDefaults(
Map<String, Converter<Object, ? extends @Nullable Object>> claimTypeConverters) {
Assert.notNull(claimTypeConverters, "claimTypeConverters cannot be null");
Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
Converter<Object, ?> collectionStringConverter = getConverter(
Converter<Object, ? extends @Nullable Object> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
Converter<Object, ? extends @Nullable Object> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
Map<String, Converter<Object, ?>> claimNameToConverter = new HashMap<>();
Map<String, Converter<Object, ? extends @Nullable Object>> claimNameToConverter = new HashMap<>();
claimNameToConverter.put(JwtClaimNames.AUD, collectionStringConverter);
claimNameToConverter.put(JwtClaimNames.EXP, MappedJwtClaimSetConverter::convertInstant);
claimNameToConverter.put(JwtClaimNames.IAT, MappedJwtClaimSetConverter::convertInstant);
@ -113,11 +116,11 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
return new MappedJwtClaimSetConverter(claimNameToConverter);
}
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
private static Converter<Object, ? extends @Nullable Object> getConverter(TypeDescriptor targetDescriptor) {
return (source) -> CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, targetDescriptor);
}
private static Instant convertInstant(Object source) {
private static @Nullable Instant convertInstant(Object source) {
if (source == null) {
return null;
}
@ -126,7 +129,7 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
return result;
}
private static String convertIssuer(Object source) {
private static @Nullable String convertIssuer(Object source) {
if (source == null) {
return null;
}
@ -149,14 +152,14 @@ public final class MappedJwtClaimSetConverter implements Converter<Map<String, O
public Map<String, Object> convert(Map<String, Object> claims) {
Assert.notNull(claims, "claims cannot be null");
Map<String, Object> mappedClaims = new HashMap<>(claims);
for (Map.Entry<String, Converter<Object, ?>> entry : this.claimTypeConverters.entrySet()) {
for (Map.Entry<String, Converter<Object, ? extends @Nullable Object>> entry : this.claimTypeConverters
.entrySet()) {
String claimName = entry.getKey();
Converter<Object, ?> converter = entry.getValue();
if (converter != null) {
Object claim = claims.get(claimName);
Object mappedClaim = converter.convert(claim);
mappedClaims.compute(claimName, (key, value) -> mappedClaim);
}
Converter<Object, ? extends @Nullable Object> converter = entry.getValue();
Object claim = claims.get(claimName);
@SuppressWarnings("NullAway")
Object mappedClaim = converter.convert(claim);
mappedClaims.compute(claimName, (key, value) -> mappedClaim);
}
Instant issuedAt = (Instant) mappedClaims.get(JwtClaimNames.IAT);
Instant expiresAt = (Instant) mappedClaims.get(JwtClaimNames.EXP);

View File

@ -57,6 +57,7 @@ import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.cache.Cache;
import org.springframework.cache.support.NoOpCache;
@ -231,7 +232,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
Map<String, Object> configuration = JwtDecoderProviderConfigurationUtils
.getConfigurationForIssuerLocation(issuer, rest);
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
return configuration.get("jwks_uri").toString();
Object jwksUri = configuration.get("jwks_uri");
Assert.notNull(jwksUri, "The public JWK Set URI must not be null");
return jwksUri.toString();
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
}
@ -494,7 +497,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private final String jwkSetUri;
private JWKSet jwkSet;
private @Nullable JWKSet jwkSet;
private SpringJWKSource(RestOperations restOperations, Cache cache, String jwkSetUri) {
Assert.notNull(restOperations, "restOperations cannot be null");
@ -518,6 +521,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, URI.create(this.jwkSetUri));
ResponseEntity<String> response = this.restOperations.exchange(request, String.class);
String jwks = response.getBody();
Assert.notNull(jwks, "JWK Set response body must not be null");
this.jwkSet = JWKSet.parse(jwks);
return jwks;
}
@ -531,13 +535,18 @@ public final class NimbusJwtDecoder implements JwtDecoder {
this.cache.invalidate();
}
this.cache.get(this.jwkSetUri, this::fetchJwks);
Assert.notNull(this.jwkSet, "JWK Set must not be null");
return this.jwkSet;
}
catch (Cache.ValueRetrievalException ex) {
if (ex.getCause() instanceof RemoteKeySourceException keys) {
Throwable cause = ex.getCause();
if (cause instanceof RemoteKeySourceException keys) {
throw keys;
}
throw new RemoteKeySourceException(ex.getCause().getMessage(), ex.getCause());
if (cause != null) {
throw new RemoteKeySourceException(cause.getMessage(), cause);
}
throw new RemoteKeySourceException(ex.getMessage(), null);
}
finally {
this.reentrantLock.unlock();

View File

@ -60,6 +60,7 @@ import com.nimbusds.jose.util.Base64;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
@ -223,8 +224,10 @@ public final class NimbusJwtEncoder implements JwtEncoder {
return signedJwt.serialize();
}
private static JWKMatcher createJwkMatcher(JwsHeader headers) {
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(headers.getAlgorithm().getName());
private static @Nullable JWKMatcher createJwkMatcher(JwsHeader headers) {
JwsAlgorithm algorithm = headers.getAlgorithm();
Assert.notNull(algorithm, "JWS header algorithm must not be null");
JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(algorithm.getName());
if (JWSAlgorithm.Family.RSA.contains(jwsAlgorithm) || JWSAlgorithm.Family.EC.contains(jwsAlgorithm)) {
// @formatter:off
@ -283,7 +286,9 @@ public final class NimbusJwtEncoder implements JwtEncoder {
}
private static JWSHeader convert(JwsHeader headers) {
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(headers.getAlgorithm().getName()));
JwsAlgorithm algorithm = headers.getAlgorithm();
Assert.notNull(algorithm, "JWS header algorithm must not be null");
JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(algorithm.getName()));
if (headers.getJwkSetUrl() != null) {
builder.jwkURL(convertAsURI(JoseHeaderNames.JKU, headers.getJwkSetUrl()));

View File

@ -55,6 +55,7 @@ import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
@ -239,7 +240,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
catch (IllegalStateException ex) {
return Mono.error(ex);
}
return Mono.just(configuration.get("jwks_uri").toString());
Object jwksUri = configuration.get("jwks_uri");
Assert.notNull(jwksUri, "The public JWK Set URI must not be null");
return Mono.just(jwksUri.toString());
}),
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
}
@ -290,7 +293,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
}
private static <C extends SecurityContext> JWTClaimsSet createClaimsSet(JWTProcessor<C> jwtProcessor,
JWT parsedToken, C context) {
JWT parsedToken, @Nullable C context) {
try {
return jwtProcessor.process(parsedToken, context);
}

View File

@ -108,7 +108,9 @@ public final class ReactiveJwtDecoders {
private static ReactiveJwtDecoder withProviderConfiguration(Map<String, Object> configuration, String issuer) {
JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer);
OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
String jwkSetUri = configuration.get("jwks_uri").toString();
Object jwksUri = configuration.get("jwks_uri");
Assert.notNull(jwksUri, "The public JWK Set URI must not be null");
String jwkSetUri = jwksUri.toString();
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri)
.jwtProcessorCustomizer(ReactiveJwtDecoderProviderConfigurationUtils::addJWSAlgorithms)
.build();

View File

@ -27,6 +27,7 @@ import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKMatcher;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.JWKSet;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import org.springframework.util.Assert;
@ -134,17 +135,12 @@ class ReactiveRemoteJWKSource implements ReactiveJWKSource {
* @param jwkMatcher The JWK matcher. Must not be {@code null}.
* @return The first key ID, {@code null} if none.
*/
protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) {
protected static @Nullable String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) {
Set<String> keyIDs = jwkMatcher.getKeyIDs();
if (keyIDs == null || keyIDs.isEmpty()) {
return null;
}
for (String id : keyIDs) {
if (id != null) {
return id;
}
}
return null; // No kid in matcher
return keyIDs.iterator().next();
}
void setWebClient(WebClient webClient) {

View File

@ -24,6 +24,7 @@ import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
@ -118,7 +119,7 @@ final class X509CertificateThumbprintValidator implements OAuth2TokenValidator<J
private static final class DefaultX509CertificateSupplier implements Supplier<X509Certificate> {
@Override
public X509Certificate get() {
public @Nullable X509Certificate get() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return null;

View File

@ -17,4 +17,7 @@
/**
* Core classes and interfaces providing support for JSON Web Token (JWT).
*/
@NullMarked
package org.springframework.security.oauth2.jwt;
import org.jspecify.annotations.NullMarked;