@@ -110,6 +111,18 @@
devkit-core
${project.version}
+
+
+ cn.org.codecrafters
+ guid
+ ${project.version}
+
+
+
+ cn.org.codecrafters
+ simple-jwt-facade
+ ${project.version}
+
diff --git a/simple-jwt-authzero/pom.xml b/simple-jwt-authzero/pom.xml
new file mode 100644
index 0000000..2b5ff33
--- /dev/null
+++ b/simple-jwt-authzero/pom.xml
@@ -0,0 +1,54 @@
+
+
+
+
+ 4.0.0
+
+ cn.org.codecrafters
+ jdevkit
+ 1.0.0
+
+
+ simple-jwt-authzero
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+ cn.org.codecrafters
+ guid
+
+
+
+ cn.org.codecrafters
+ simple-jwt-facade
+
+
+
+ com.auth0
+ java-jwt
+
+
+
+
\ No newline at end of file
diff --git a/simple-jwt-authzero/src/main/java/cn/org/codecrafters/simplejwt/authzero/AuthzeroTokenResolver.java b/simple-jwt-authzero/src/main/java/cn/org/codecrafters/simplejwt/authzero/AuthzeroTokenResolver.java
new file mode 100644
index 0000000..7e4e52d
--- /dev/null
+++ b/simple-jwt-authzero/src/main/java/cn/org/codecrafters/simplejwt/authzero/AuthzeroTokenResolver.java
@@ -0,0 +1,461 @@
+/*
+ * Copyright (C) 2023 CodeCraftersCN.
+ *
+ * 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 cn.org.codecrafters.simplejwt.authzero;
+
+import cn.org.codecrafters.devkit.guid.GuidCreator;
+import cn.org.codecrafters.simplejwt.TokenPayload;
+import cn.org.codecrafters.simplejwt.TokenResolver;
+import cn.org.codecrafters.simplejwt.annotations.ExcludeFromPayload;
+import cn.org.codecrafters.simplejwt.authzero.config.AuthzeroTokenResolverConfig;
+import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTCreator;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.interfaces.JWTVerifier;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.InvocationTargetException;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+
+/**
+ * The {@code AuthzeroTokenResolver} class is an implementation of the {@link
+ * cn.org.codecrafters.simplejwt.TokenResolver} interface. It uses the {@code
+ * com.auth0:java-jwt} library to handle JSON Web Token (JWT) resolution. This
+ * resolver provides functionality to create, extract, verify, and renew JWT
+ * tokens using various algorithms and custom payload data.
+ *
+ * Dependencies:
+ * This implementation relies on the {@code com.auth0:java-jwt} library. Please
+ * ensure you have added this library as a dependency to your project before
+ * using this resolver.
+ *
+ * Usage:
+ * To use the {@code AuthzeroTokenResolver}, first, create an instance of this
+ * class:
+ *
{@code
+ * TokenResolver tokenResolver =
+ * new AuthzeroTokenResolver(TokenAlgorithm.HS256,
+ * "Token Subject",
+ * "Token Issuer",
+ * "Token Secret");
+ * }
+ *
+ * Then, you can utilize the various methods provided by this resolver to
+ * handle JWT tokens:
+ *
+ *
{@code
+ * // Creating a new JWT token
+ * String token =
+ * tokenResolver.createToken(Duration.ofHours(1),
+ * "your_subject",
+ * "your_audience",
+ * customPayloads);
+ *
+ * // Extracting payload data from a JWT token
+ * DecodedJWT decodedJWT = tokenResolver.resolve(token);
+ * T payloadData = decodedJWT.extract(token, T.class);
+ *
+ * // Renewing an existing JWT token
+ * String renewedToken =
+ * tokenResolver.renew(token, Duration.ofMinutes(30), customPayloads);
+ * }
+ *
+ * Note:
+ * It is essential to configure the appropriate algorithms, secret, and issuer
+ * according to your specific use case when using this resolver.
+ * Additionally, ensure that the {@code com.auth0:java-jwt} library is
+ * correctly configured in your project's dependencies.
+ *
+ * @author Zihlu Wang
+ * @version 1.0.0
+ * @see GuidCreator
+ * @see Algorithm
+ * @see JWTVerifier
+ * @see JWTCreator
+ * @see JWTCreator.Builder
+ * @since 1.0.0
+ */
+@Slf4j
+public class AuthzeroTokenResolver implements TokenResolver {
+
+ /**
+ * GuidCreator used for generating unique identifiers for "jti" claim in
+ * JWT tokens.
+ */
+ private final GuidCreator> jtiCreator;
+
+ /**
+ * The algorithm used for signing and verifying JWT tokens.
+ */
+ private final Algorithm algorithm;
+
+ /**
+ * The issuer claim value to be included in JWT tokens.
+ */
+ private final String issuer;
+
+ /**
+ * The JSON Web Token resolver.
+ */
+ private final JWTVerifier verifier;
+
+ /**
+ * Creates a new instance of AuthzeroTokenResolver with the provided
+ * configurations.
+ *
+ * @param jtiCreator the 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 secret the secret used for HMAC-based algorithms (HS256,
+ * HS384, HS512) for token signing and verification
+ */
+ public AuthzeroTokenResolver(GuidCreator> jtiCreator, TokenAlgorithm algorithm, String issuer, String secret) {
+ if (secret == null || secret.isBlank()) {
+ throw new IllegalArgumentException("A secret is required to build a JSON Web Token.");
+ }
+ this.jtiCreator = jtiCreator;
+ this.algorithm = AuthzeroTokenResolverConfig.getInstance().getFunction(algorithm).apply(secret);
+ this.issuer = issuer;
+ this.verifier = JWT.require(this.algorithm).build();
+ }
+
+ /**
+ * Creates a new instance of 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 secret the secret used for HMAC-based algorithms (HS256,
+ * HS384, HS512) for token signing and verification
+ */
+ public AuthzeroTokenResolver(TokenAlgorithm algorithm, String issuer, String secret) {
+ this((GuidCreator) UUID::randomUUID, algorithm, issuer, secret);
+ }
+
+ /**
+ * Creates a new instance of 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 HMAC-based algorithms (HS256,
+ * HS384, HS512) for token signing and verification
+ */
+ public AuthzeroTokenResolver(String issuer, String secret) {
+ this(TokenAlgorithm.HS256, issuer, secret);
+ }
+
+ /**
+ * Builds the basic information of the JSON Web Token (JWT) using the
+ * provided parameters and adds it to the JWTCreator.Builder.
+ *
+ * @param subject the subject claim value to be included in the JWT
+ * @param audience an array of audience claim values to be included in
+ * the JWT
+ * @param expireAfter the duration after which the JWT will expire
+ * @param builder the JWTCreator.Builder instance to which the basic
+ * information will be added
+ */
+ private void buildBasicInfo(JWTCreator.Builder builder, Duration expireAfter, String subject, String... audience) {
+ var now = LocalDateTime.now();
+
+ // bind issuer (iss)
+ builder.withIssuer(issuer);
+ // bind issued at (iat)
+ builder.withIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()));
+ // bind not before (nbf)
+ builder.withNotBefore(Date.from(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()));
+ // bind JWT Id (jti)
+ builder.withJWTId(jtiCreator.nextId().toString());
+ }
+
+ /**
+ * Add a claim to a builder.
+ *
+ * @param builder the builder to build this JSON Web Token
+ * @param name the property name
+ * @param value the property value
+ */
+ private void addClaim(JWTCreator.Builder builder, String name, Object value) {
+ if (Objects.nonNull(value)) {
+ if (value instanceof Boolean v) {
+ builder.withClaim(name, v);
+ } else if (value instanceof Double v) {
+ builder.withClaim(name, v);
+ } else if (value instanceof Float v) {
+ builder.withClaim(name, v.doubleValue());
+ } else if (value instanceof Integer v) {
+ builder.withClaim(name, v);
+ } else if (value instanceof Long v) {
+ builder.withClaim(name, v);
+ } else if (value instanceof String v) {
+ builder.withClaim(name, v);
+ } else if (value instanceof Date v) {
+ builder.withClaim(name, v);
+ } else if (value instanceof List> v) {
+ builder.withClaim(name, v);
+ } else {
+ log.warn("""
+ Unable to determine the type of field {}, converting it to a string now.
+ """, name);
+ builder.withClaim(name, value.toString());
+ }
+ } else {
+ builder.withNullClaim(name);
+ }
+ }
+
+ /**
+ * Builds the custom claims of the JSON Web Token (JWT) using the provided
+ * Map of claims and adds them to the JWTCreator.Builder.
+ *
+ * This method is used to add custom claims to the JWT. It takes a Map of
+ * claims, where each entry represents a custom claim name (key) and its
+ * corresponding value (value). The custom claims will be added to the JWT
+ * using the JWTCreator.Builder.
+ *
+ * @param claims a Map containing the custom claims to be added to the JWT
+ * @param builder the JWTCreator.Builder instance to which the custom
+ * claims will be added
+ */
+ private void buildMapClaims(JWTCreator.Builder builder, Map claims) {
+ if (Objects.nonNull(claims)) {
+ for (var e : claims.entrySet()) {
+ addClaim(builder, e.getKey(), e.getValue());
+ }
+ }
+ }
+
+ /**
+ * Finish creating a token.
+ *
+ * This is the final step of create a token, to sign this token.
+ *
+ * @param builder the builder to build this JWT
+ * @return the generated token as a {@code String}
+ */
+ private String buildToken(JWTCreator.Builder builder) {
+ return builder.sign(algorithm);
+ }
+
+ /**
+ * Creates a new token with the specified expiration time, subject, and
+ * audience.
+ *
+ * @param expireAfter the duration after which the token will expire
+ * @param subject the subject of the token
+ * @param audience the audience for which the token is intended
+ * @return the generated token as a {@code String}
+ */
+ @Override
+ public String createToken(Duration expireAfter, String audience, String subject) {
+ final var builder = JWT.create();
+ buildBasicInfo(builder, expireAfter, subject, audience);
+ return buildToken(builder);
+ }
+
+ /**
+ * Creates a new token with the specified expiration time, subject,
+ * audience, and custom payload data.
+ *
+ * @param expireAfter the duration after which the token will expire
+ * @param subject the subject of the token
+ * @param audience the audience for which the token is intended
+ * @param payloads the custom payload data to be included in the token
+ * @return the generated token as a {@code String}
+ */
+ @Override
+ public String createToken(Duration expireAfter, String audience, String subject, Map payloads) {
+ // Create token.
+ final var builder = JWT.create();
+ buildBasicInfo(builder, expireAfter, subject, audience);
+ buildMapClaims(builder, payloads);
+ return buildToken(builder);
+ }
+
+ /**
+ * Creates a new token with the specified expiration time, subject,
+ * audience, and strongly-typed payload data.
+ *
+ * @param expireAfter the duration after which the token will expire
+ * @param subject the subject of the token
+ * @param audience the audience for which the token is intended
+ * @param payload the strongly-typed payload data to be included in the
+ * token
+ * @return the generated token as a {@code String}
+ */
+ @Override
+ public String createToken(Duration expireAfter, String audience, String subject, T payload) {
+ final JWTCreator.Builder builder = JWT.create();
+ buildBasicInfo(builder, expireAfter, subject, audience);
+
+ var payloadClass = payload.getClass();
+ var fields = payloadClass.getDeclaredFields();
+
+ for (var field : fields) {
+ // Skip the fields which are annotated with ExcludeFromPayload
+ if (field.isAnnotationPresent(ExcludeFromPayload.class))
+ continue;
+
+ try {
+ field.setAccessible(true);
+ // Build Claims
+ addClaim(builder, field.getName(), field.get(payload));
+ } catch (IllegalAccessException e) {
+ log.error("Cannot access field %s!".formatted(field.getName()));
+ }
+ }
+
+ return buildToken(builder);
+ }
+
+ /**
+ * Resolves the given token into a DecodedJWT object.
+ *
+ * @param token the token to be resolved
+ * @return a ResolvedToken object
+ */
+ @Override
+ public DecodedJWT resolve(String token) {
+ return verifier.verify(token);
+ }
+
+ /**
+ * Extracts the payload information from the given token and maps it to the
+ * specified target type.
+ *
+ * @param token the token from which to extract the payload
+ * @param targetType the target class representing the payload data type
+ * @return an instance of the specified target type with the extracted
+ * payload data, or {@code null} when extraction fails
+ */
+ @Override
+ public T extract(String token, Class targetType) {
+ // Get claims from token.
+ var claims = resolve(token).getClaims();
+
+ try {
+ // Get the no-argument constructor to create an instance.
+ T bean = targetType.getConstructor().newInstance();
+
+ var fields = targetType.getDeclaredFields();
+ for (var field : fields) {
+ // Ignore the field annotated with @ExcludeFromPayload.
+ if (field.isAnnotationPresent(ExcludeFromPayload.class))
+ continue;
+
+ // Get the name of this field.
+ var fieldName = field.getName();
+
+ // Prevent this class is annotated @Slf4j or added logger.
+ if ("log".equalsIgnoreCase(fieldName) || "logger".equalsIgnoreCase(fieldName))
+ continue;
+
+ // Get the value of this field.
+ var fieldValue = Optional.ofNullable(claims.get(fieldName))
+ .map(claim -> claim.as(field.getType()))
+ .orElse(null);
+ if (fieldValue != null) {
+ // Set the field value by invoking the setter method.
+ var setter = targetType.getDeclaredMethod("set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1), fieldValue.getClass());
+ setter.invoke(bean, fieldValue);
+ }
+ }
+
+ return bean;
+ } catch (NoSuchMethodException e) {
+ log.error("Unable to find a no-argument constructor declaration for class %s.".formatted(targetType.getCanonicalName()));
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ log.error("Unable to create a new instance of class %s.".formatted(targetType.getCanonicalName()));
+ }
+ return null;
+ }
+
+ /**
+ * Renews the given expired token with the specified custom payload data.
+ *
+ * @param oldToken the expired token to be renewed
+ * @param payload the custom payload data to be included in the renewed
+ * token
+ * @return the renewed token as a {@code String}
+ */
+ @Override
+ public String renew(String oldToken, Duration expireAfter, Map payload) {
+ final var resolvedToken = this.resolve(oldToken);
+ var audience = resolvedToken.getAudience().get(0);
+
+ return createToken(expireAfter, audience, resolvedToken.getSubject(), payload);
+ }
+
+ /**
+ * Renews the given expired token with the specified custom payload data.
+ *
+ * @param oldToken the expired token to be renewed
+ * @param payload the custom payload data to be included in the renewed
+ * token
+ * @return the renewed token as a {@code String}
+ */
+ @Override
+ public String renew(String oldToken, Map payload) {
+ return renew(oldToken, Duration.ofMinutes(30), payload);
+ }
+
+ /**
+ * Renews the given expired token with the new specified strongly-typed
+ * payload data.
+ *
+ * @param oldToken the expired token to be renewed
+ * @param payload the strongly-typed payload data to be included in the
+ * renewed token
+ * @return the renewed token as a {@code String}
+ */
+ @Override
+ public String renew(String oldToken, Duration expireAfter, T payload) {
+ final var resolvedToken = this.resolve(oldToken);
+ var audience = resolvedToken.getAudience().get(0);
+
+ return createToken(expireAfter, audience, resolvedToken.getSubject(), payload);
+ }
+
+ /**
+ * Renews the given expired token with the new specified strongly-typed
+ * payload data.
+ *
+ * @param the type of the payload data, must implement
+ * {@link TokenPayload}
+ * @param oldToken the expired token to be renewed
+ * @param payload the strongly-typed payload data to be included in the
+ * renewed token
+ * @return the renewed token as a {@code String}
+ */
+ @Override
+ public String renew(String oldToken, T payload) {
+ return renew(oldToken, Duration.ofMinutes(30), payload);
+ }
+}
diff --git a/simple-jwt-authzero/src/main/java/cn/org/codecrafters/simplejwt/authzero/config/AuthzeroTokenResolverConfig.java b/simple-jwt-authzero/src/main/java/cn/org/codecrafters/simplejwt/authzero/config/AuthzeroTokenResolverConfig.java
new file mode 100644
index 0000000..e78ee2c
--- /dev/null
+++ b/simple-jwt-authzero/src/main/java/cn/org/codecrafters/simplejwt/authzero/config/AuthzeroTokenResolverConfig.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 CodeCraftersCN.
+ *
+ * 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 cn.org.codecrafters.simplejwt.authzero.config;
+
+import cn.org.codecrafters.simplejwt.config.TokenResolverConfig;
+import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
+import cn.org.codecrafters.simplejwt.exceptions.UnsupportedAlgorithmException;
+import com.auth0.jwt.algorithms.Algorithm;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * The AuthzeroTokenResolverConfig class provides the configuration for the
+ * AuthzeroTokenResolver.
+ *
+ * This configuration class is used to establish the mapping between the
+ * standard TokenAlgorithm defined within the AuthzeroTokenResolver facade and
+ * the specific algorithms used by the Auth0 Java JWT library, which is the
+ * underlying library used by AuthzeroTokenResolver to handle JSON Web Tokens
+ * (JWTs).
+ *
+ *
+ * Algorithm Mapping:
+ * The AuthzeroTokenResolverConfig class allows specifying the relationship
+ * between the standard TokenAlgorithm instances supported by
+ * AuthzeroTokenResolver and the corresponding algorithms used by the
+ * com.auth0:java-jwt library. The mapping is achieved using a Map, where the
+ * keys are the standard TokenAlgorithm instances, and the values represent the
+ * algorithm functions used by Auth0 Java JWT library for each corresponding
+ * key.
+ *
+ *
+ * Note:
+ * The provided algorithm mapping should be consistent with the actual
+ * algorithms supported and used by the Auth0 Java JWT library. It is crucial
+ * to ensure that the mapping is accurate to enable proper token validation
+ * and processing within the AuthzeroTokenResolver.
+ *
+ * @author Zihlu Wang
+ * @version 1.0.0
+ * @since 1.0.0
+ */
+public final class AuthzeroTokenResolverConfig implements TokenResolverConfig> {
+
+ /**
+ * Constructs a new instance of AuthzeroTokenResolverConfig.
+ *
+ * The constructor is set as private to enforce the singleton pattern for
+ * this configuration class. Instances of AuthzeroTokenResolverConfig
+ * should be obtained through the {@link #getInstance()} method.
+ */
+ private AuthzeroTokenResolverConfig() {
+ }
+
+ /**
+ * The singleton instance of AuthzeroTokenResolverConfig.
+ *
+ * This instance is used to ensure that only one instance of
+ * AuthzeroTokenResolverConfig is created and shared throughout the
+ * application. The singleton pattern is implemented to provide centralized
+ * configuration and avoid redundant object creation.
+ */
+ private static AuthzeroTokenResolverConfig instance;
+
+ /**
+ * The supported algorithms and their corresponding algorithm functions.
+ *
+ * This map stores the supported algorithms as keys and their corresponding
+ * algorithm functions as values. The algorithm functions represent the
+ * functions used by the Auth0 Java JWT library to handle the specific
+ * algorithms. The mapping is used to provide proper algorithm resolution
+ * and processing within the AuthzeroTokenResolver.
+ */
+ private static final Map> SUPPORTED_ALGORITHMS = new HashMap<>() {{
+ put(TokenAlgorithm.HS256, Algorithm::HMAC256);
+ put(TokenAlgorithm.HS384, Algorithm::HMAC384);
+ put(TokenAlgorithm.HS512, Algorithm::HMAC512);
+ }};
+
+ /**
+ * Gets the instance of AuthzeroTokenResolverConfig.
+ *
+ * This method returns the singleton instance of
+ * 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 AuthzeroTokenResolverConfig
+ */
+ public static AuthzeroTokenResolverConfig getInstance() {
+ if (Objects.isNull(instance)) {
+ instance = new AuthzeroTokenResolverConfig();
+ }
+
+ return instance;
+ }
+
+ /**
+ * Gets the algorithm function corresponding to the specified
+ * TokenAlgorithm.
+ *
+ * This method returns the algorithm function associated with the given
+ * TokenAlgorithm. The provided TokenAlgorithm represents the specific
+ * algorithm for which the corresponding algorithm function is required.
+ * The returned AlgorithmFunction represents the function implementation
+ * that can be used by the TokenResolver to handle the specific algorithm.
+ *
+ * @param algorithm the TokenAlgorithm for which the algorithm function is
+ * required
+ * @return the algorithm function associated with the given TokenAlgorithm
+ * @throws UnsupportedAlgorithmException if the given {@code algorithm} is
+ * not supported by this
+ * implementation
+ */
+ @Override
+ public Function getFunction(TokenAlgorithm algorithm) {
+ return Optional.of(SUPPORTED_ALGORITHMS).map((entry) -> entry.get(algorithm))
+ .orElseThrow(() -> new UnsupportedAlgorithmException("The specified algorithm is not supported yet."));
+ }
+}
diff --git a/simple-jwt-authzero/src/main/resources/logback.xml b/simple-jwt-authzero/src/main/resources/logback.xml
new file mode 100644
index 0000000..af1c7a3
--- /dev/null
+++ b/simple-jwt-authzero/src/main/resources/logback.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} : %msg%n
+
+
+
+
+
+ %date{yyyy-MM-dd HH:mm:ss} [%thread] %highlight(%-5level) %cyan(%logger{50}) : %msg%n
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/simple-jwt-authzero/src/test/java/cn/org/codecrafters/simplejwt/authzero/test/TestAuthzeroTokenResolver.java b/simple-jwt-authzero/src/test/java/cn/org/codecrafters/simplejwt/authzero/test/TestAuthzeroTokenResolver.java
new file mode 100644
index 0000000..52e2f18
--- /dev/null
+++ b/simple-jwt-authzero/src/test/java/cn/org/codecrafters/simplejwt/authzero/test/TestAuthzeroTokenResolver.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 CodeCraftersCN.
+ *
+ * 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 cn.org.codecrafters.simplejwt.authzero.test;
+
+import cn.org.codecrafters.simplejwt.authzero.AuthzeroTokenResolver;
+import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+
+/**
+ * TestAuthzeroTokenResolver
+ *
+ * @author Zihlu Wang
+ */
+public class TestAuthzeroTokenResolver {
+
+ @Test
+ public void test01() {
+ var tokenResolver = new AuthzeroTokenResolver(TokenAlgorithm.HS384, "Test Issuer", "Test Secret");
+ var testToken = tokenResolver.createToken(Duration.ofMinutes(30), "Test Audience", "User00001");
+ System.out.println(testToken);
+ }
+
+}