feat: added ECDSA-based algorithm support

make the simple-jwt auth0 implementation can use ECDSA-based algorithms

BREAKING CHANGE: the io.jsonwebtoken:jjwt implementation was
discontinued since its design made a big challenge to encapsulation
This commit is contained in:
zihluwang
2024-08-06 22:02:00 +08:00
parent 62b8cb8118
commit fe88788611
21 changed files with 344 additions and 1477 deletions
@@ -19,13 +19,11 @@ package com.onixbyte.simplejwt.authzero;
import com.onixbyte.devkit.utils.Base64Util;
import com.onixbyte.guid.GuidCreator;
import com.onixbyte.simplejwt.SecretCreator;
import com.onixbyte.security.KeyLoader;
import com.onixbyte.simplejwt.TokenPayload;
import com.onixbyte.simplejwt.TokenResolver;
import com.onixbyte.simplejwt.annotations.ExcludeFromPayload;
import com.onixbyte.simplejwt.annotations.TokenEnum;
import com.onixbyte.simplejwt.authzero.config.AuthzeroTokenResolverConfig;
import com.onixbyte.simplejwt.config.TokenResolverConfig;
import com.onixbyte.simplejwt.constants.PredefinedKeys;
import com.onixbyte.simplejwt.constants.TokenAlgorithm;
import com.auth0.jwt.JWT;
@@ -37,13 +35,20 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.onixbyte.simplejwt.exceptions.IllegalKeyPairException;
import com.onixbyte.simplejwt.exceptions.IllegalSecretException;
import com.onixbyte.simplejwt.exceptions.UnsupportedAlgorithmException;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationTargetException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* The {@code AuthzeroTokenResolver} class is an implementation of the {@link TokenResolver}
@@ -97,110 +102,160 @@ import java.util.*;
public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
/**
* Creates a new instance of {@code AuthzeroTokenResolver} with the provided configurations.
* Create a builder of {@link AuthzeroTokenResolver}.
*
* @param jtiCreator the {@link GuidCreator} used for generating unique identifiers for "jti"
* claim in JWT tokens
* @param algorithm the algorithm used for signing and verifying JWT tokens
* @param issuer the issuer claim value to be included in JWT tokens
* @param privateKey the secret used for HMAC-based algorithms (HS256, HS384, HS512) for
* token signing and verification, or the private key for ECDSA-based
* algorithms
* @param publicKey the public key for ECDSA-based algorithms
* @param objectMapper JSON handler
* @return a builder instance
*/
public AuthzeroTokenResolver(GuidCreator<?> jtiCreator,
TokenAlgorithm algorithm,
String issuer,
String privateKey,
String publicKey,
ObjectMapper objectMapper) {
if (TokenResolverConfig.HMAC_ALGORITHMS.contains(algorithm)) {
if (privateKey == null || privateKey.isBlank()) {
throw new IllegalArgumentException("A secret is required to build a JSON Web Token.");
}
public static Builder builder() {
return new Builder();
}
if (privateKey.length() < 32) {
log.warn("The provided secret which owns {} characters is too weak. Please consider" +
" replacing it with a stronger one.", privateKey.length());
}
/**
* Builder for {@link AuthzeroTokenResolver}
*/
public static class Builder {
/**
* GuidCreator used for generating unique identifiers for "jti" claim in JWT tokens.
*/
private GuidCreator<?> jtiCreator;
/**
* The algorithm used for signing and verifying JWT tokens.
*/
private Algorithm algorithm;
/**
* The issuer claim value to be included in JWT tokens.
*/
private String issuer;
/**
* Jackson JSON handler.
*/
private ObjectMapper objectMapper;
/**
* The secret to sign a JWT with HMAC based algorithm.
*/
private String secret;
/**
* The private key to sign a JWT with ECDSA based algorithm.
*/
private ECPrivateKey privateKey;
/**
* The public key to read a JWT with ECDSA based algorithm.
*/
private ECPublicKey publicKey;
/**
* Private constructor prevents this class being initialised at somewhere it should not
* be initialised.
*/
private Builder() {
}
this.jtiCreator = jtiCreator;
this.algorithm = config
.getAlgorithm(algorithm)
.apply(privateKey, publicKey);
this.issuer = issuer;
this.verifier = JWT.require(this.algorithm).build();
this.objectMapper = objectMapper;
}
/**
* Set the secret to sign a JWT.
*
* @param secret the secret
* @return the builder instance
*/
public Builder secret(String secret) {
this.secret = secret;
return this;
}
/**
* Creates a new instance of {@link AuthzeroTokenResolver} with the provided configurations
* and a simple UUID GuidCreator.
*
* @param algorithm the algorithm used for signing and verifying JWT tokens
* @param issuer the issuer claim value to be included in JWT tokens
* @param privateKey the secret used for HMAC-based algorithms (HS256, HS384, HS512) for
* token signing and verification, or the private key for ECDSA-based
* algorithms
* @param publicKey the public key for ECDSA-based algorithms
* @param objectMapper Jackson Databind JSON Handler
*/
public AuthzeroTokenResolver(TokenAlgorithm algorithm,
String issuer,
String privateKey,
String publicKey,
ObjectMapper objectMapper) {
this(UUID::randomUUID, algorithm, issuer, privateKey, publicKey, objectMapper);
}
/**
* Set the key pair to sign a JWT.
*
* @param publicKey the pem formatted public key text
* @param privateKey the pem formatted private key text
* @return the builder instance
*/
public Builder keyPair(String publicKey, String privateKey) {
this.publicKey = KeyLoader.loadEcdsaPublicKey(publicKey);
this.privateKey = KeyLoader.loadEcdsaPrivateKey(privateKey);
return this;
}
/**
* Creates a new instance of {@link AuthzeroTokenResolver} with the provided configurations
* and a simple UUID GuidCreator.
*
* @param algorithm the algorithm used for signing and verifying JWT tokens
* @param issuer the issuer claim value to be included in JWT tokens
* @param privateKey the secret used for HMAC-based algorithms (HS256, HS384, HS512) for
* token signing and verification, or the private key for ECDSA-based
* algorithms
* @param publicKey the public key for ECDSA-based algorithms
*/
public AuthzeroTokenResolver(TokenAlgorithm algorithm, String issuer, String privateKey, String publicKey) {
this(UUID::randomUUID, algorithm, issuer, privateKey, publicKey, new ObjectMapper());
}
/**
* Set the algorithm to sign a JWT.
* <p>
* A secret required by HMAC-based algorithms, or key pair required by ECDSA-based
* algorithms need to be set before initialise an algorithm.
*
* @param algorithm an {@link TokenAlgorithm} value
* @return the builder instance
*/
public Builder algorithm(TokenAlgorithm algorithm) {
// check the secret or key pair before algorithm initialised
if (HMAC_ALGORITHMS.containsKey(algorithm)) {
if (Objects.isNull(secret) || secret.isBlank()) {
throw new IllegalSecretException("""
Please specify a secret before define an algorithm.""");
}
} else if (ECDSA_ALGORITHMS.containsKey(algorithm)) {
if (Objects.isNull(publicKey) || Objects.isNull(privateKey)) {
throw new IllegalKeyPairException("""
Please specify a ECDSA key pair before define an algorithm.""");
}
}
/**
* Creates a new instance of {@link AuthzeroTokenResolver} with the
* provided configurations, HMAC256 algorithm and a simple
* UUID GuidCreator.
*
* @param issuer the issuer claim value to be included in JWT tokens
* @param secret the secret used for HS256 algorithms for token signing and verification
*/
public AuthzeroTokenResolver(String issuer, String secret) {
this(UUID::randomUUID, TokenAlgorithm.HS256, issuer, secret, "", new ObjectMapper());
}
// initialise algorithm
this.algorithm = switch (algorithm) {
case HS256, HS384, HS512 -> HMAC_ALGORITHMS.get(algorithm).apply(secret);
case ES256, ES384, ES512 -> ECDSA_ALGORITHMS.get(algorithm)
.apply(publicKey, privateKey);
default -> throw new UnsupportedAlgorithmException("""
This algorithm is not supported yet.""");
};
return this;
}
/**
* Creates a new instance of {@link AuthzeroTokenResolver} with the
* provided configurations, HMAC256 algorithm and a simple
* UUID GuidCreator.
*
* @param issuer the issuer claim value to be included in JWT tokens
*/
public AuthzeroTokenResolver(String issuer) {
var secret = SecretCreator.createSecret(32, true, true, true);
/**
* Set the object mapper.
*
* @param objectMapper an object mapper
* @return the builder instance
*/
public Builder objectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
return this;
}
this.jtiCreator = UUID::randomUUID;
this.algorithm = config
.getAlgorithm(TokenAlgorithm.HS256)
.apply(secret, "");
this.issuer = issuer;
this.verifier = JWT.require(this.algorithm).build();
this.objectMapper = new ObjectMapper();
/**
* Set the creator of JWT id.
*
* @param jtiCreator a creator to create JWT id
* @return the builder instance
*/
public Builder jtiCreator(GuidCreator<?> jtiCreator) {
this.jtiCreator = jtiCreator;
return this;
}
log.info("The secret has been set to {}.", secret);
/**
* Set the issuer of created JWT.
*
* @param issuer the person or organisation issued this JWT
* @return the builder instance
*/
public Builder issuer(String issuer) {
this.issuer = issuer;
return this;
}
/**
* Create an {@link AuthzeroTokenResolver} instance
*
* @return created instance
*/
public AuthzeroTokenResolver build() {
return new AuthzeroTokenResolver(jtiCreator, algorithm, issuer, objectMapper);
}
}
/**
@@ -251,7 +306,7 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
*/
@Override
public <T extends TokenPayload> String createToken(Duration expireAfter, String audience, String subject, T payload) {
final JWTCreator.Builder builder = JWT.create();
final var builder = JWT.create();
buildBasicInfo(builder, expireAfter, subject, audience);
var payloadClass = payload.getClass();
@@ -294,7 +349,7 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
*/
@Override
public DecodedJWT resolve(String token) {
return verifier.verify(token);
return jwtVerifier.verify(token);
}
/**
@@ -450,15 +505,15 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
// bind issuer (iss)
builder.withIssuer(issuer);
// bind issued at (iat)
builder.withIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()));
builder.withIssuedAt(now.atZone(ZoneId.systemDefault()).toInstant());
// bind not before (nbf)
builder.withNotBefore(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()));
builder.withNotBefore(now.atZone(ZoneId.systemDefault()).toInstant());
// bind audience (aud)
builder.withAudience(audience);
// bind subject (sub)
builder.withSubject(subject);
// bind expire at (exp)
builder.withExpiresAt(Date.from(now.plus(expireAfter).atZone(ZoneId.systemDefault()).toInstant()));
builder.withExpiresAt(now.plus(expireAfter).atZone(ZoneId.systemDefault()).toInstant());
// bind JWT Id (jti)
builder.withJWTId(jtiCreator.nextId().toString());
}
@@ -534,14 +589,13 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
/**
* Default type reference for Map.
*/
private static class MapTypeReference extends TypeReference<Map<String, Object>> {
public static class MapTypeReference extends TypeReference<Map<String, Object>> {
MapTypeReference() {
}
}
/**
* GuidCreator used for generating unique identifiers for "jti" claim in
* JWT tokens.
* GuidCreator used for generating unique identifiers for "jti" claim in JWT tokens.
*/
private final GuidCreator<?> jtiCreator;
@@ -558,12 +612,45 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
/**
* The JSON Web Token resolver.
*/
private final JWTVerifier verifier;
private final JWTVerifier jwtVerifier;
/**
* Jackson JSON handler.
*/
private final ObjectMapper objectMapper;
private final AuthzeroTokenResolverConfig config = AuthzeroTokenResolverConfig.getInstance();
/**
* A map contains all HMAC-SHA based algorithms.
*/
public static final Map<TokenAlgorithm, Function<String, Algorithm>> HMAC_ALGORITHMS = Map.of(
TokenAlgorithm.HS256, Algorithm::HMAC256,
TokenAlgorithm.HS384, Algorithm::HMAC384,
TokenAlgorithm.HS512, Algorithm::HMAC512
);
/**
* A map contains all ECDSA based algorithms.
*/
public static final Map<TokenAlgorithm, BiFunction<ECPublicKey, ECPrivateKey, Algorithm>> ECDSA_ALGORITHMS = Map.of(
TokenAlgorithm.ES256, Algorithm::ECDSA256,
TokenAlgorithm.ES384, Algorithm::ECDSA384,
TokenAlgorithm.ES512, Algorithm::ECDSA512
);
/**
* Private constructor prevent this class being initialised mistakenly.
*
* @param jtiCreator a creator that can create JWT id
* @param algorithm an algorithm to sign this JWT
* @param issuer the person or organisation who issued this JWT
* @param objectMapper a mapper for handling JSON serialisation and deserialization
*/
private AuthzeroTokenResolver(GuidCreator<?> jtiCreator, Algorithm algorithm, String issuer, ObjectMapper objectMapper) {
this.jtiCreator = jtiCreator;
this.algorithm = algorithm;
this.issuer = issuer;
this.objectMapper = objectMapper;
this.jwtVerifier = JWT.require(algorithm).build();
}
}
@@ -1,148 +0,0 @@
/*
* Copyright (C) 2024-2024 OnixByte.
*
* 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
*
* http://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.
*/
package com.onixbyte.simplejwt.authzero.config;
import com.onixbyte.security.KeyLoader;
import com.onixbyte.simplejwt.TokenResolver;
import com.onixbyte.simplejwt.authzero.AuthzeroTokenResolver;
import com.onixbyte.simplejwt.config.TokenResolverConfig;
import com.onixbyte.simplejwt.constants.TokenAlgorithm;
import com.onixbyte.simplejwt.exceptions.UnsupportedAlgorithmException;
import com.auth0.jwt.algorithms.Algorithm;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* The {@code AuthzeroTokenResolverConfig} class provides the configuration for
* the {@link AuthzeroTokenResolver}.
* <p>
* This configuration is used to establish the mapping between the standard {@link TokenAlgorithm}
* defined in the {@code cn.org.codecrafters:simple-jwt-facade} and the specific algorithms used
* by the {@code com.auth0:java-jwt} library, which is the underlying library used by
* {@link AuthzeroTokenResolver} to handle JSON Web Tokens (JWTs).
* <p>
* <b>Algorithm Mapping:</b>
* The {@code AuthzeroTokenResolverConfig} allows specifying the relationships between the standard
* {@link TokenAlgorithm} instances supported by {@link AuthzeroTokenResolver} and the corresponding
* algorithms used by the {@code com.auth0:java-jwt} library. The mapping is achieved using a Map,
* where the keys are the standard {@link TokenAlgorithm} instances, and the values represent the
* algorithm functions used by {@code com.auth0:java-jwt} library for each corresponding key.
* <p>
* <b>Note:</b>
* The provided algorithm mapping should be consistent with the actual algorithms supported and used
* by the {@code com.auth0:java-jwt} library. It is crucial to ensure that the mapping is accurate
* to enable proper token validation and processing within the {@link AuthzeroTokenResolver}.
*
* @author Zihlu Wang
* @version 1.1.1
* @since 1.0.0
*/
public final class AuthzeroTokenResolverConfig
implements TokenResolverConfig<BiFunction<String, String, Algorithm>> {
/**
* Gets the instance of {@code AuthzeroTokenResolverConfig}.
* <p>
* This method returns the singleton instance of {@code AuthzeroTokenResolverConfig}. If the
* instance is not yet created, it will create a new instance and return it. Otherwise, it
* returns the existing instance.
*
* @return the instance of {@code AuthzeroTokenResolverConfig}
*/
public static AuthzeroTokenResolverConfig getInstance() {
if (Objects.isNull(instance)) {
instance = new AuthzeroTokenResolverConfig();
}
return instance;
}
/**
* Gets the algorithm function corresponding to the specified {@link TokenAlgorithm}.
* <p>
* This method returns the algorithm function associated with the given {@link TokenAlgorithm}.
* The provided {@link TokenAlgorithm} represents the specific algorithm for which the
* corresponding algorithm function is required. The returned Algorithm Function represents the
* function implementation that can be used by the {@link TokenResolver} to handle the
* specific algorithm.
*
* @param algorithm the {@link TokenAlgorithm} for which the algorithm function is required
* @return the algorithm function associated with the given {@link TokenAlgorithm}
* @throws UnsupportedAlgorithmException if the given {@code algorithm} is not supported by
* this implementation
*/
@Override
public BiFunction<String, String, Algorithm> getAlgorithm(TokenAlgorithm algorithm) {
return Optional.of(SUPPORTED_ALGORITHMS).map((entry) -> entry.get(algorithm))
.orElseThrow(() -> new UnsupportedAlgorithmException("The specified algorithm is not supported yet."));
}
/**
* Constructs a new instance of {@code AuthzeroTokenResolverConfig}.
* <p>
* The constructor is set as private to enforce the singleton pattern for
* this configuration class. Instances of
* {@code AuthzeroTokenResolverConfig} should be obtained through the
* {@link #getInstance()} method.
*/
private AuthzeroTokenResolverConfig() {
}
/**
* The singleton instance of {@code AuthzeroTokenResolverConfig}.
* <p>
* This instance is used to ensure that only one instance of
* {@code AuthzeroTokenResolverConfig} is created and shared throughout the
* application. The singleton pattern is implemented to provide centralised
* configuration and avoid redundant object creation.
*/
private static AuthzeroTokenResolverConfig instance;
/**
* The supported algorithms and their corresponding algorithm functions.
* <p>
* This map stores the supported algorithms as keys and their corresponding
* algorithm functions as values. The algorithm functions represent the
* functions used by the {@code com.auth0:java-jwt} library to handle the
* specific algorithms. The mapping is used to provide proper algorithm
* resolution and processing within the {@link AuthzeroTokenResolver}.
*/
private static final
Map<TokenAlgorithm, BiFunction<String, String, Algorithm>> SUPPORTED_ALGORITHMS =
new HashMap<>() {{
put(TokenAlgorithm.HS256, (String secret, String ignoredValue) ->
Algorithm.HMAC256(secret));
put(TokenAlgorithm.HS384, (String secret, String ignoredValue) ->
Algorithm.HMAC384(secret));
put(TokenAlgorithm.HS512, (String secret, String ignoredValue) ->
Algorithm.HMAC512(secret));
put(TokenAlgorithm.ES256, (String privateKey, String publicKey) ->
Algorithm.ECDSA256(KeyLoader.loadEcdsaPrivateKey(privateKey)));
put(TokenAlgorithm.ES384, (String privateKey, String publicKey) ->
Algorithm.ECDSA256(KeyLoader.loadEcdsaPrivateKey(privateKey)));
put(TokenAlgorithm.ES512, (String privateKey, String publicKey) ->
Algorithm.ECDSA256(KeyLoader.loadEcdsaPrivateKey(privateKey)));
}};
}
@@ -1,54 +0,0 @@
/*
* Copyright (C) 2024-2024 OnixByte.
*
* 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
*
* http://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.
*/
/**
* The package {@code cn.org.codecrafters.simplejwt.authzero.config} contains
* configuration classes related to the {@link
* com.onixbyte.simplejwt.authzero.AuthzeroTokenResolver}
* implementation.
* <p>
* The classes in this package provide configuration options and settings for
* the {@link com.onixbyte.simplejwt.authzero.AuthzeroTokenResolver},
* which is used for resolving JSON Web Tokens (JWT) using the Auth0 library.
* <p>
* The {@link
* com.onixbyte.simplejwt.authzero.config.AuthzeroTokenResolverConfig}
* class is a configuration class that defines the mapping between standard
* {@link com.onixbyte.simplejwt.constants.TokenAlgorithm} and the
* corresponding function implementation used by {@link
* com.onixbyte.simplejwt.authzero.AuthzeroTokenResolver} for handling
* JWT algorithms. It enables developers to specify and customize the
* algorithm functions according to the chosen JWT algorithm and the library
* being used.
* <p>
* The configuration options in this package help developers integrate and
* configure the {@link
* com.onixbyte.simplejwt.authzero.AuthzeroTokenResolver} seamlessly
* into their Spring Boot applications. Developers can fine-tune the token
* resolution process and customize algorithm handling to align with their
* specific requirements and desired level of security.
* <p>
* It is recommended to explore the classes in this package to understand how
* to configure and use the {@link
* com.onixbyte.simplejwt.authzero.AuthzeroTokenResolver} effectively
* in the Spring Boot environment to handle JWT authentication and
* authorisation securely and efficiently.
*
* @since 1.0.0
*/
package com.onixbyte.simplejwt.authzero.config;