All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.org.codecrafters.simplejwt.jjwt.JjwtTokenResolver Maven / Gradle / Ivy

/*
 * 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.annotations.ExcludeFromPayload;
import cn.org.codecrafters.simplejwt.annotations.TokenEnum;
import cn.org.codecrafters.simplejwt.constants.PredefinedKeys;
import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
import cn.org.codecrafters.simplejwt.exceptions.WeakSecretException;
import cn.org.codecrafters.simplejwt.jjwt.config.JjwtTokenResolverConfig;
import com.fasterxml.jackson.core.type.TypeReference;
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.*;

/**
 * The {@link JjwtTokenResolver} class is an implementation of the {@link
 * cn.org.codecrafters.simplejwt.TokenResolver} interface. It uses the {@code
 * io.jsonwebtoken:jjwt} 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.
 * 

* Usage: * To use the {@code JjwtTokenResolver}, first, create an instance of this * class: *

{@code
 *   TokenResolver> tokenResolver =
 *       new JjwtTokenResolver(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 io.jsonwebtoken:jjwt} library is * correctly configured in your project's dependencies. * * @author Zihlu Wang * @version 1.1.0 * @see Claims * @see Jws * @see Jwts * @see SignatureAlgorithm * @see Keys * @since 1.0.0 */ @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 (Objects.isNull(secret) || 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, Map claims) { var now = LocalDateTime.now(); 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.addClaims(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) { return buildToken(expireAfter, audience, subject, 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) { return buildToken(expireAfter, audience, subject, 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 fields = payload.getClass().getDeclaredFields(); var payloadMap = new HashMap(); for (var field : fields) { if (field.isAnnotationPresent(ExcludeFromPayload.class)) continue; try { var getter = payload.getClass().getDeclaredMethod("get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1)); // Build Claims /* * Note (17 Oct, 2023): The jjwt can only add a map to be added. */ var fieldValue = getter.invoke(payload); // Handle enum fields. if (field.isAnnotationPresent(TokenEnum.class)) { var annotation = field.getAnnotation(TokenEnum.class); var enumGetter = field.getType().getDeclaredMethod("get" + annotation.propertyName().substring(0, 1).toUpperCase() + annotation.propertyName().substring(1)); fieldValue = enumGetter.invoke(fieldValue); } payloadMap.put(field.getName(), fieldValue); } catch (IllegalAccessException | NoSuchMethodException e) { log.error("Cannot access field {}!", field.getName()); } catch (InvocationTargetException e) { log.error("Cannot invoke getter.", e); } } return buildToken(expireAfter, audience, subject, payloadMap); } /** * Resolves the given token into a {@link Jws} 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 { var bean = targetType.getConstructor().newInstance(); for (var entry : claims.entrySet()) { // Jump all JWT pre-defined properties and the fields that are annotated to be excluded. if (PredefinedKeys.KEYS.contains(entry.getKey()) || targetType.getDeclaredField(entry.getKey()).isAnnotationPresent(ExcludeFromPayload.class)) continue; var field = targetType.getDeclaredField(entry.getKey()); var fieldValue = entry.getValue(); if (field.isAnnotationPresent(TokenEnum.class)) { var annotation = field.getAnnotation(TokenEnum.class); var enumStaticLoader = field.getType().getDeclaredMethod("loadBy" + annotation.propertyName().substring(0, 1).toUpperCase() + annotation.propertyName().substring(1), annotation.dataType().getMappedClass()); fieldValue = enumStaticLoader.invoke(null, entry.getValue()); } var setter = targetType.getDeclaredMethod("set" + entry.getKey().substring(0, 1).toUpperCase() + entry.getKey().substring(1), fieldValue.getClass()); if (setter.canAccess(bean)) { setter.invoke(bean, fieldValue); } else { log.error("Setter for field {} can't be accessed.", entry.getKey()); } } return bean; } catch (InvocationTargetException e) { log.error("Target is not invokable.", e); } catch (NoSuchMethodException e) { log.error("Cannot find method according to given data.", e); } 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.", e); } catch (NoSuchFieldException e) { log.error("Cannot load field according to given field name.", e); } return null; } /** * Re-generate a new token with the payload in the old one. * * @param oldToken the old token * @param expireAfter how long the new token can be valid for * @return re-generated token with the payload in the old one */ @Override public String renew(String oldToken, Duration expireAfter) { var resolvedToken = resolve(oldToken); var tokenPayloads = resolvedToken.getBody(); var audience = tokenPayloads.getAudience(); var subject = tokenPayloads.getSubject(); PredefinedKeys.KEYS.forEach(tokenPayloads::remove); return createToken(expireAfter, audience, subject, tokenPayloads); } /** * 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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy