) UUID::randomUUID;
- this.algorithm = AuthzeroTokenResolverConfig.getInstance()
+ this.algorithm = config
.getAlgorithm(TokenAlgorithm.HS256)
.apply(secret);
this.issuer = issuer;
diff --git a/simple-jwt-facade/src/main/java/cn/org/codecrafters/simplejwt/TokenResolver.java b/simple-jwt-facade/src/main/java/cn/org/codecrafters/simplejwt/TokenResolver.java
index a4c9ff0..65189d3 100644
--- a/simple-jwt-facade/src/main/java/cn/org/codecrafters/simplejwt/TokenResolver.java
+++ b/simple-jwt-facade/src/main/java/cn/org/codecrafters/simplejwt/TokenResolver.java
@@ -17,6 +17,7 @@
package cn.org.codecrafters.simplejwt;
+import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
import java.util.Map;
@@ -25,7 +26,7 @@ import java.util.Map;
* The {@code TokenResolver} interface defines methods for creating,
* extracting, and renewing tokens, particularly JSON Web Tokens (JWTs). It
* provides a set of methods to generate tokens with various payload
- * configurations, extract payloads from tokens, and renew expired tokens.
+ * configurations, extract payload from tokens, and renew expired tokens.
*
*
* Token Creation:
@@ -72,10 +73,10 @@ public interface TokenResolver {
* @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
+ * @param payload the custom payload data to be included in the token
* @return the generated token as a {@code String}
*/
- String createToken(Duration expireAfter, String audience, String subject, Map payloads);
+ String createToken(Duration expireAfter, String audience, String subject, Map payload);
/**
* Creates a new token with the specified expiration time, subject,
@@ -113,16 +114,6 @@ public interface TokenResolver {
*/
T extract(String token, Class targetType);
- /**
- * 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}
- */
- String renew(String oldToken, Map payload);
-
/**
* Renews the given expired token with the specified custom payload data.
*
@@ -134,6 +125,16 @@ public interface TokenResolver {
*/
String renew(String oldToken, Duration expireAfter, Map 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}
+ */
+ String renew(String oldToken, Map payload);
+
/**
* Renews the given expired token with the specified strongly-typed
* payload data.
diff --git a/simple-jwt-jjwt/pom.xml b/simple-jwt-jjwt/pom.xml
new file mode 100644
index 0000000..85c66e5
--- /dev/null
+++ b/simple-jwt-jjwt/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ 4.0.0
+
+ cn.org.codecrafters
+ jdevkit
+ 1.0.0
+
+
+ simple-jwt-jjwt
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+ cn.org.codecrafters
+ devkit-utils
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+
+
+
+ io.jsonwebtoken
+ jjwt-impl
+
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+
+
+
+ cn.org.codecrafters
+ simple-jwt-facade
+
+
+
+
\ No newline at end of file
diff --git a/simple-jwt-jjwt/src/main/java/cn/org/codecrafters/simplejwt/jjwt/JjwtTokenResolver.java b/simple-jwt-jjwt/src/main/java/cn/org/codecrafters/simplejwt/jjwt/JjwtTokenResolver.java
new file mode 100644
index 0000000..f38c287
--- /dev/null
+++ b/simple-jwt-jjwt/src/main/java/cn/org/codecrafters/simplejwt/jjwt/JjwtTokenResolver.java
@@ -0,0 +1,313 @@
+/*
+ * 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.jjwt;
+
+import cn.org.codecrafters.devkit.utils.MapUtil;
+import cn.org.codecrafters.guid.GuidCreator;
+import cn.org.codecrafters.simplejwt.SecretCreator;
+import cn.org.codecrafters.simplejwt.TokenPayload;
+import cn.org.codecrafters.simplejwt.TokenResolver;
+import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
+import cn.org.codecrafters.simplejwt.exceptions.WeakSecretException;
+import cn.org.codecrafters.simplejwt.jjwt.config.JjwtTokenResolverConfig;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.InvocationTargetException;
+import java.nio.charset.StandardCharsets;
+import java.security.Key;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * JjwtTokenResolver
+ *
+ * @author Zihlu Wang
+ */
+@Slf4j
+public class JjwtTokenResolver implements TokenResolver> {
+
+ private final GuidCreator> jtiCreator;
+
+ private final SignatureAlgorithm algorithm;
+
+ private final String issuer;
+
+ private final Key key;
+
+ private final JjwtTokenResolverConfig config = JjwtTokenResolverConfig.getInstance();
+
+ public JjwtTokenResolver(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.");
+ }
+
+ if (secret.length() <= 32) {
+ log.error("""
+ The provided secret which owns {} characters is too weak. Please replace it with a stronger one.
+ """, secret.length());
+ throw new WeakSecretException("""
+ The provided secret which owns %s characters is too weak. Please replace it with a stronger one.
+ """.formatted(secret.length()));
+ }
+
+ this.jtiCreator = jtiCreator;
+ this.algorithm = config.getAlgorithm(algorithm);
+ this.issuer = issuer;
+ this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public JjwtTokenResolver(TokenAlgorithm algorithm, String issuer, String secret) {
+ if (secret == null || secret.isBlank()) {
+ throw new IllegalArgumentException("A secret is required to build a JSON Web Token.");
+ }
+
+ if (secret.length() <= 32) {
+ log.error("""
+ The provided secret which owns {} characters is too weak. Please replace it with a stronger one.
+ """, secret.length());
+ throw new WeakSecretException("""
+ The provided secret which owns %s characters is too weak. Please replace it with a stronger one.
+ """.formatted(secret.length()));
+ }
+
+ this.jtiCreator = UUID::randomUUID;
+ this.algorithm = config.getAlgorithm(algorithm);
+ this.issuer = issuer;
+ this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public JjwtTokenResolver(String issuer, String secret) {
+ if (secret == null || secret.isBlank()) {
+ throw new IllegalArgumentException("A secret is required to build a JSON Web Token.");
+ }
+
+ if (secret.length() <= 32) {
+ log.error("""
+ The provided secret which owns {} characters is too weak. Please replace it with a stronger one.
+ """, secret.length());
+ throw new WeakSecretException("""
+ The provided secret which owns %s characters is too weak. Please replace it with a stronger one.
+ """.formatted(secret.length()));
+ }
+
+ this.jtiCreator = UUID::randomUUID;
+ this.algorithm = config.getAlgorithm(TokenAlgorithm.HS256);
+ this.issuer = issuer;
+ this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public JjwtTokenResolver(String issuer) {
+ this.jtiCreator = UUID::randomUUID;
+ this.algorithm = config.getAlgorithm(TokenAlgorithm.HS256);
+ this.issuer = issuer;
+ this.key = Keys.hmacShaKeyFor(SecretCreator.createSecret(32, true, true, true).getBytes(StandardCharsets.UTF_8));
+ }
+
+ private String buildToken(Duration expireAfter, String audience, String subject, LocalDateTime now, Map claims) {
+ var builder = Jwts.builder()
+ .setHeaderParam("typ", "JWT")
+ .setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
+ .setNotBefore(Date.from(now.atZone(ZoneId.systemDefault()).toInstant()))
+ .setExpiration(Date.from(now.plus(expireAfter).atZone(ZoneId.systemDefault()).toInstant()))
+ .setSubject(subject)
+ .setAudience(audience)
+ .setIssuer(this.issuer)
+ .setId(jtiCreator.nextId().toString());
+
+ if (claims != null && !claims.isEmpty()) {
+ builder.setClaims(claims);
+ }
+
+ return builder.signWith(key, algorithm)
+ .compact();
+ }
+
+ /**
+ * Creates a new token with the specified expiration time, subject, and
+ * audience.
+ *
+ * @param expireAfter the duration after which the token will expire
+ * @param audience the audience for which the token is intended
+ * @param subject the subject of the token
+ * @return the generated token as a {@code String}
+ */
+ @Override
+ public String createToken(Duration expireAfter, String audience, String subject) {
+ var now = LocalDateTime.now();
+ return buildToken(expireAfter, audience, subject, now, null);
+ }
+
+ /**
+ * 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 audience the audience for which the token is intended
+ * @param subject the subject of the token
+ * @param payload 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 payload) {
+ var now = LocalDateTime.now();
+ return buildToken(expireAfter, audience, subject, now, payload);
+ }
+
+ /**
+ * 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 audience the audience for which the token is intended
+ * @param subject the subject of the token
+ * @param payload the strongly-typed payload data to be included in the
+ * token
+ * @return the generated token as a {@code String} or {@code null} if
+ * creation fails
+ * @see MapUtil#objectToMap(Object)
+ */
+ @Override
+ public String createToken(Duration expireAfter, String audience, String subject, T payload) {
+ var now = LocalDateTime.now();
+ try {
+ var claims = MapUtil.objectToMap(payload);
+ return buildToken(expireAfter, audience, subject, now, claims);
+ } catch (IllegalAccessException e) {
+ log.error("An error occurs while accessing the fields of the object");
+ }
+
+ return null;
+ }
+
+ /**
+ * Resolves the given token into a ResolvedTokenType object.
+ *
+ * @param token the token to be resolved
+ * @return a ResolvedTokenType object
+ */
+ @Override
+ public Jws resolve(String token) {
+ return Jwts.parserBuilder()
+ .setSigningKey(key)
+ .build()
+ .parseClaimsJws(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} if extraction fails.
+ * @see MapUtil#mapToObject(Map, Class)
+ */
+ @Override
+ public T extract(String token, Class targetType) {
+ var resolvedToken = resolve(token);
+
+ var claims = resolvedToken.getBody();
+ try {
+ return MapUtil.mapToObject(claims, targetType);
+ } catch (InvocationTargetException e) {
+ log.info("An error occurs while invoking the constructor of type {}.", targetType.getCanonicalName());
+ } catch (NoSuchMethodException e) {
+ log.error("The constructor of the required type {} is not found.", targetType.getCanonicalName());
+ } catch (InstantiationException e) {
+ log.error("The required type {} is abstract or an interface.", targetType.getCanonicalName());
+ } catch (IllegalAccessException e) {
+ log.error("An error occurs while accessing the fields of the object.");
+ }
+
+ return null;
+ }
+
+ /**
+ * Renews the given expired token with the specified custom payload data.
+ *
+ * @param oldToken the expired token to be renewed
+ * @param expireAfter specify when does the new token invalid
+ * @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) {
+ var resolvedTokenClaims = resolve(oldToken).getBody();
+ var audience = resolvedTokenClaims.getAudience();
+ var subject = resolvedTokenClaims.getSubject();
+
+ return createToken(expireAfter, audience, subject, 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 specified strongly-typed
+ * payload data.
+ *
+ * @param oldToken the expired token to be renewed
+ * @param expireAfter specify when does the new token invalid
+ * @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) {
+ var resolvedTokenClaims = resolve(oldToken).getBody();
+ var audience = resolvedTokenClaims.getAudience();
+ var subject = resolvedTokenClaims.getSubject();
+
+ return createToken(expireAfter, audience, subject, payload);
+ }
+
+ /**
+ * Renews the given expired token with the 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, T payload) {
+ return renew(oldToken, Duration.ofMinutes(30), payload);
+ }
+}
diff --git a/simple-jwt-jjwt/src/main/java/cn/org/codecrafters/simplejwt/jjwt/config/JjwtTokenResolverConfig.java b/simple-jwt-jjwt/src/main/java/cn/org/codecrafters/simplejwt/jjwt/config/JjwtTokenResolverConfig.java
new file mode 100644
index 0000000..c0e8aa2
--- /dev/null
+++ b/simple-jwt-jjwt/src/main/java/cn/org/codecrafters/simplejwt/jjwt/config/JjwtTokenResolverConfig.java
@@ -0,0 +1,76 @@
+/*
+ * 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.jjwt.config;
+
+import cn.org.codecrafters.simplejwt.config.TokenResolverConfig;
+import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
+import cn.org.codecrafters.simplejwt.exceptions.UnsupportedAlgorithmException;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JjwtTokenResolverConfig
+ *
+ * @author Zihlu Wang
+ */
+public final class JjwtTokenResolverConfig implements TokenResolverConfig {
+
+ private JjwtTokenResolverConfig() {}
+
+ private static final Map SUPPORTED_ALGORITHMS = new HashMap<>() {{
+ put(TokenAlgorithm.HS256, SignatureAlgorithm.HS256);
+ put(TokenAlgorithm.HS384, SignatureAlgorithm.HS384);
+ put(TokenAlgorithm.HS512, SignatureAlgorithm.HS512);
+ }};
+
+ private static JjwtTokenResolverConfig instance;
+
+ public static JjwtTokenResolverConfig getInstance() {
+ if (instance == null) {
+ instance = new JjwtTokenResolverConfig();
+ }
+
+ 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
+ */
+ @Override
+ public SignatureAlgorithm getAlgorithm(TokenAlgorithm algorithm) {
+ if (!SUPPORTED_ALGORITHMS.containsKey(algorithm)) {
+ throw new UnsupportedAlgorithmException("""
+ The request algorithm is not supported by our system yet. Please change to supported ones.
+ """);
+ }
+ return SUPPORTED_ALGORITHMS.get(algorithm);
+ }
+}
diff --git a/simple-jwt-spring-boot-starter/pom.xml b/simple-jwt-spring-boot-starter/pom.xml
index a4f73a8..95cd204 100644
--- a/simple-jwt-spring-boot-starter/pom.xml
+++ b/simple-jwt-spring-boot-starter/pom.xml
@@ -40,12 +40,42 @@
simple-jwt-facade