mirror of
https://github.com/spring-projects/spring-security.git
synced 2026-03-25 03:21:18 +00:00
Enable null-safety in spring-security-oauth2-jose
Closes gh-17821
This commit is contained in:
parent
78f762fab8
commit
22a98583f1
@ -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")
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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)) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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()));
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user