feat(simple-jwt-jjwt): Complete the implementation with io.jsonwebtoken:jjwt-api
This commit is contained in:
+313
@@ -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<Jws<Claims>> {
|
||||
|
||||
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<String, Object> 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<String, Object> 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 <T extends TokenPayload> 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<Claims> 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 extends TokenPayload> T extract(String token, Class<T> 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<String, Object> 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<String, Object> 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 <T extends TokenPayload> 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 <T extends TokenPayload> String renew(String oldToken, T payload) {
|
||||
return renew(oldToken, Duration.ofMinutes(30), payload);
|
||||
}
|
||||
}
|
||||
+76
@@ -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<SignatureAlgorithm> {
|
||||
|
||||
private JjwtTokenResolverConfig() {}
|
||||
|
||||
private static final Map<TokenAlgorithm, SignatureAlgorithm> 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user