From 22a98583f14bf26eb9bb1a3c0554f0a72a5aa83a Mon Sep 17 00:00:00 2001 From: Joe Grandja <10884212+jgrandja@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:51:49 -0400 Subject: [PATCH] Enable null-safety in spring-security-oauth2-jose Closes gh-17821 --- .../web/OAuth2ResourceServerDslTests.kt | 2 +- .../web/oauth2/resourceserver/JwtDslTests.kt | 2 +- .../config/web/server/ServerJwtDslTests.kt | 2 +- .../UserDetailsJwtPrincipalConverter.kt | 5 +- .../spring-security-oauth2-jose.gradle | 6 ++- .../oauth2/jose/jws/MacAlgorithm.java | 4 +- .../oauth2/jose/jws/SignatureAlgorithm.java | 4 +- .../oauth2/jose/jws/package-info.java | 3 ++ .../security/oauth2/jose/package-info.java | 24 +++++++++ .../security/oauth2/jwt/DPoPProofContext.java | 20 +++---- .../security/oauth2/jwt/JoseHeader.java | 53 ++++++++++--------- .../security/oauth2/jwt/Jwt.java | 11 ++-- .../security/oauth2/jwt/JwtClaimAccessor.java | 33 +++++++----- .../oauth2/jwt/JwtClaimValidator.java | 6 ++- .../JwtDecoderProviderConfigurationUtils.java | 1 + .../security/oauth2/jwt/JwtDecoders.java | 4 +- .../oauth2/jwt/JwtEncoderParameters.java | 10 ++-- .../jwt/MappedJwtClaimSetConverter.java | 35 ++++++------ .../security/oauth2/jwt/NimbusJwtDecoder.java | 17 ++++-- .../security/oauth2/jwt/NimbusJwtEncoder.java | 11 ++-- .../oauth2/jwt/NimbusReactiveJwtDecoder.java | 7 ++- .../oauth2/jwt/ReactiveJwtDecoders.java | 4 +- .../oauth2/jwt/ReactiveRemoteJWKSource.java | 10 ++-- .../X509CertificateThumbprintValidator.java | 3 +- .../security/oauth2/jwt/package-info.java | 3 ++ 25 files changed, 178 insertions(+), 102 deletions(-) create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/package-info.java diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDslTests.kt index 3de7c51cf2..2e23b9f9a1 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/OAuth2ResourceServerDslTests.kt @@ -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") diff --git a/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/JwtDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/JwtDslTests.kt index 63dbca8e03..8cef1eee82 100644 --- a/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/JwtDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/annotation/web/oauth2/resourceserver/JwtDslTests.kt @@ -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() } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt index e500eecba8..373fa1d069 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt @@ -168,7 +168,7 @@ class ServerJwtDslTests { } class NullReactiveJwtDecoder: ReactiveJwtDecoder { - override fun decode(token: String?): Mono { + override fun decode(token: String): Mono { return Mono.empty() } } diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.kt index d45027f9c9..3db070b724 100644 --- a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.kt +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/oauth2/resourceserver/customuserdetailsservice/UserDetailsJwtPrincipalConverter.kt @@ -29,7 +29,8 @@ import org.springframework.stereotype.Component class UserDetailsJwtPrincipalConverter(private val users: UserDetailsService) : Converter { 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 = jwt.claims diff --git a/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle b/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle index 0517dae907..c98aa7910c 100644 --- a/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle +++ b/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle @@ -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")) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/MacAlgorithm.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/MacAlgorithm.java index c53db6dedd..7074e09424 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/MacAlgorithm.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/MacAlgorithm.java @@ -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; diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java index d12e466aff..764601892c 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java @@ -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; diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/package-info.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/package-info.java index 50fdc49287..5b40d61e6f 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/package-info.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/package-info.java @@ -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; diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/package-info.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/package-info.java new file mode 100644 index 0000000000..f895c86e50 --- /dev/null +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/package-info.java @@ -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; diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofContext.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofContext.java index 711a27dcc0..16fd54ecc4 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofContext.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofContext.java @@ -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 getAccessToken() { + public @Nullable 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)) { diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java index af9da6f7ae..b286b8b9cc 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JoseHeader.java @@ -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 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 getJwk() { + public @Nullable Map 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 getX509CertificateChain() { + public @Nullable List 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 Google @@ -128,7 +132,7 @@ class JoseHeader { * the first SHA1 collision */ @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 getCritical() { + public @Nullable Set getCritical() { return getHeader(JoseHeaderNames.CRIT); } @@ -180,10 +185,10 @@ class JoseHeader { * Returns the header value. * @param name the header name * @param 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 getHeader(String name) { + public @Nullable T getHeader(String name) { Assert.hasText(name, "name cannot be empty"); return (T) getHeaders().get(name); } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java index 5b3f4c165d..71d5ceafbb 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/Jwt.java @@ -24,6 +24,8 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.AbstractOAuth2Token; import org.springframework.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 headers, + public Jwt(String tokenValue, @Nullable Instant issuedAt, @Nullable Instant expiresAt, Map headers, Map 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"); } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimAccessor.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimAccessor.java index 746e72ea88..0b98972624 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimAccessor.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimAccessor.java @@ -20,6 +20,8 @@ import java.net.URL; import java.time.Instant; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.security.oauth2.core.ClaimAccessor; /** @@ -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 getAudience() { + default @Nullable List 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); } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java index 2d62a47501..54ec78dd70 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java @@ -62,8 +62,10 @@ public final class JwtClaimValidator implements OAuth2TokenValidator { 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); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java index d6027c83fa..b678c2b620 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java @@ -164,6 +164,7 @@ final class JwtDecoderProviderConfigurationUtils { RequestEntity request = RequestEntity.get(uri.toUriString()).build(); ResponseEntity> response = rest.exchange(request, STRING_OBJECT_MAP); Map 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; } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java index 295bfb8a98..89181e9e89 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoders.java @@ -109,7 +109,9 @@ public final class JwtDecoders { private static JwtDecoder withProviderConfiguration(Map configuration, String issuer) { JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); OAuth2TokenValidator 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(); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoderParameters.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoderParameters.java index 8e65ec211f..d9f0222ba2 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoderParameters.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtEncoderParameters.java @@ -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; } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/MappedJwtClaimSetConverter.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/MappedJwtClaimSetConverter.java index 6089f87899..32425b5789 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/MappedJwtClaimSetConverter.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/MappedJwtClaimSetConverter.java @@ -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> claimTypeConverters; + private final Map> claimTypeConverters; /** * Constructs a {@link MappedJwtClaimSetConverter} with the provided arguments @@ -62,7 +64,7 @@ public final class MappedJwtClaimSetConverter implements Converter> claimTypeConverters) { + public MappedJwtClaimSetConverter(Map> claimTypeConverters) { Assert.notNull(claimTypeConverters, "claimTypeConverters cannot be null"); this.claimTypeConverters = claimTypeConverters; } @@ -96,12 +98,13 @@ public final class MappedJwtClaimSetConverter implements Converter> claimTypeConverters) { + public static MappedJwtClaimSetConverter withDefaults( + Map> claimTypeConverters) { Assert.notNull(claimTypeConverters, "claimTypeConverters cannot be null"); - Converter stringConverter = getConverter(STRING_TYPE_DESCRIPTOR); - Converter collectionStringConverter = getConverter( + Converter stringConverter = getConverter(STRING_TYPE_DESCRIPTOR); + Converter collectionStringConverter = getConverter( TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR)); - Map> claimNameToConverter = new HashMap<>(); + Map> 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 getConverter(TypeDescriptor targetDescriptor) { + private static Converter 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 convert(Map claims) { Assert.notNull(claims, "claims cannot be null"); Map mappedClaims = new HashMap<>(claims); - for (Map.Entry> entry : this.claimTypeConverters.entrySet()) { + for (Map.Entry> entry : this.claimTypeConverters + .entrySet()) { String claimName = entry.getKey(); - Converter converter = entry.getValue(); - if (converter != null) { - Object claim = claims.get(claimName); - Object mappedClaim = converter.convert(claim); - mappedClaims.compute(claimName, (key, value) -> mappedClaim); - } + Converter 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); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java index 8688791008..5fb07130e1 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java @@ -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 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 request = new RequestEntity<>(headers, HttpMethod.GET, URI.create(this.jwkSetUri)); ResponseEntity 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(); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java index 6160f8602e..9f6ee0eba5 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java @@ -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())); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java index 2b8e58c549..4e103f2daa 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java @@ -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 JWTClaimsSet createClaimsSet(JWTProcessor jwtProcessor, - JWT parsedToken, C context) { + JWT parsedToken, @Nullable C context) { try { return jwtProcessor.process(parsedToken, context); } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java index b6d37496c5..2f5dae3657 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.java @@ -108,7 +108,9 @@ public final class ReactiveJwtDecoders { private static ReactiveJwtDecoder withProviderConfiguration(Map configuration, String issuer) { JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); OAuth2TokenValidator 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(); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java index ac0abf54a7..e976328623 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/ReactiveRemoteJWKSource.java @@ -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 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) { diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/X509CertificateThumbprintValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/X509CertificateThumbprintValidator.java index 75adc72295..7bbf119079 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/X509CertificateThumbprintValidator.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/X509CertificateThumbprintValidator.java @@ -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 { @Override - public X509Certificate get() { + public @Nullable X509Certificate get() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes == null) { return null; diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/package-info.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/package-info.java index 2ce8f5b0e6..5ff30c2e65 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/package-info.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/package-info.java @@ -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;