1717
1818package cn .org .codecrafters .simplejwt .authzero ;
1919
20+ import cn .org .codecrafters .devkit .utils .Base64Util ;
2021import cn .org .codecrafters .guid .GuidCreator ;
2122import cn .org .codecrafters .simplejwt .SecretCreator ;
2223import cn .org .codecrafters .simplejwt .TokenPayload ;
2324import cn .org .codecrafters .simplejwt .TokenResolver ;
2425import cn .org .codecrafters .simplejwt .annotations .ExcludeFromPayload ;
26+ import cn .org .codecrafters .simplejwt .annotations .TokenEnum ;
2527import cn .org .codecrafters .simplejwt .authzero .config .AuthzeroTokenResolverConfig ;
2628import cn .org .codecrafters .simplejwt .config .TokenResolverConfig ;
29+ import cn .org .codecrafters .simplejwt .constants .PredefinedKeys ;
2730import cn .org .codecrafters .simplejwt .constants .TokenAlgorithm ;
2831import com .auth0 .jwt .JWT ;
2932import com .auth0 .jwt .JWTCreator ;
3033import com .auth0 .jwt .algorithms .Algorithm ;
34+ import com .auth0 .jwt .interfaces .Claim ;
3135import com .auth0 .jwt .interfaces .DecodedJWT ;
3236import com .auth0 .jwt .interfaces .JWTVerifier ;
37+ import com .fasterxml .jackson .core .JsonProcessingException ;
38+ import com .fasterxml .jackson .core .type .TypeReference ;
39+ import com .fasterxml .jackson .databind .JsonMappingException ;
40+ import com .fasterxml .jackson .databind .JsonNode ;
41+ import com .fasterxml .jackson .databind .ObjectMapper ;
42+ import com .fasterxml .jackson .databind .node .ObjectNode ;
3343import lombok .extern .slf4j .Slf4j ;
3444
45+ import java .lang .reflect .Field ;
3546import java .lang .reflect .InvocationTargetException ;
3647import java .time .Duration ;
3748import java .time .LocalDateTime ;
@@ -114,21 +125,27 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
114125 */
115126 private final JWTVerifier verifier ;
116127
128+ /**
129+ * Jackson JSON handler.
130+ */
131+ private final ObjectMapper objectMapper ;
132+
117133 private final AuthzeroTokenResolverConfig config = AuthzeroTokenResolverConfig .getInstance ();
118134
119135 /**
120136 * Creates a new instance of {@code AuthzeroTokenResolver} with the
121137 * provided configurations.
122138 *
123- * @param jtiCreator the {@link GuidCreator} used for generating unique
124- * identifiers for "jti" claim in JWT tokens
125- * @param algorithm the algorithm used for signing and verifying JWT
126- * tokens
127- * @param issuer the issuer claim value to be included in JWT tokens
128- * @param secret the secret used for HMAC-based algorithms (HS256,
129- * HS384, HS512) for token signing and verification
139+ * @param jtiCreator the {@link GuidCreator} used for generating unique
140+ * identifiers for "jti" claim in JWT tokens
141+ * @param algorithm the algorithm used for signing and verifying JWT
142+ * tokens
143+ * @param issuer the issuer claim value to be included in JWT tokens
144+ * @param secret the secret used for HMAC-based algorithms (HS256,
145+ * HS384, HS512) for token signing and verification
146+ * @param objectMapper JSON handler
130147 */
131- public AuthzeroTokenResolver (GuidCreator <?> jtiCreator , TokenAlgorithm algorithm , String issuer , String secret ) {
148+ public AuthzeroTokenResolver (GuidCreator <?> jtiCreator , TokenAlgorithm algorithm , String issuer , String secret , ObjectMapper objectMapper ) {
132149 if (secret == null || secret .isBlank ()) {
133150 throw new IllegalArgumentException ("A secret is required to build a JSON Web Token." );
134151 }
@@ -143,6 +160,21 @@ public AuthzeroTokenResolver(GuidCreator<?> jtiCreator, TokenAlgorithm algorithm
143160 .apply (secret );
144161 this .issuer = issuer ;
145162 this .verifier = JWT .require (this .algorithm ).build ();
163+ this .objectMapper = objectMapper ;
164+ }
165+
166+ /**
167+ * Creates a new instance of {@link AuthzeroTokenResolver} with the
168+ * provided configurations and a simple UUID GuidCreator.
169+ *
170+ * @param algorithm the algorithm used for signing and verifying JWT tokens
171+ * @param issuer the issuer claim value to be included in JWT tokens
172+ * @param secret the secret used for HMAC-based algorithms (HS256,
173+ * HS384, HS512) for token signing and verification
174+ * @param objectMapper Jackson Databind JSON Handler
175+ */
176+ public AuthzeroTokenResolver (TokenAlgorithm algorithm , String issuer , String secret , ObjectMapper objectMapper ) {
177+ this (UUID ::randomUUID , algorithm , issuer , secret , objectMapper );
146178 }
147179
148180 /**
@@ -155,20 +187,7 @@ public AuthzeroTokenResolver(GuidCreator<?> jtiCreator, TokenAlgorithm algorithm
155187 * HS384, HS512) for token signing and verification
156188 */
157189 public AuthzeroTokenResolver (TokenAlgorithm algorithm , String issuer , String secret ) {
158- if (secret == null || secret .isBlank ()) {
159- throw new IllegalArgumentException ("A secret is required to build a JSON Web Token." );
160- }
161-
162- if (secret .length () <= 32 ) {
163- log .warn ("The provided secret which owns {} characters is too weak. Please consider replacing it with a stronger one." , secret .length ());
164- }
165-
166- this .jtiCreator = UUID ::randomUUID ;
167- this .algorithm = config
168- .getAlgorithm (algorithm )
169- .apply (secret );
170- this .issuer = issuer ;
171- this .verifier = JWT .require (this .algorithm ).build ();
190+ this (UUID ::randomUUID , algorithm , issuer , secret , new ObjectMapper ());
172191 }
173192
174193 /**
@@ -181,20 +200,7 @@ public AuthzeroTokenResolver(TokenAlgorithm algorithm, String issuer, String sec
181200 * HS384, HS512) for token signing and verification
182201 */
183202 public AuthzeroTokenResolver (String issuer , String secret ) {
184- if (secret == null || secret .isBlank ()) {
185- throw new IllegalArgumentException ("A secret is required to build a JSON Web Token." );
186- }
187-
188- if (secret .length () <= 32 ) {
189- log .warn ("The provided secret which owns {} characters is too weak. Please consider replacing it with a stronger one." , secret .length ());
190- }
191-
192- this .jtiCreator = UUID ::randomUUID ;
193- this .algorithm = config
194- .getAlgorithm (TokenAlgorithm .HS256 )
195- .apply (secret );
196- this .issuer = issuer ;
197- this .verifier = JWT .require (this .algorithm ).build ();
203+ this (UUID ::randomUUID , TokenAlgorithm .HS256 , issuer , secret , new ObjectMapper ());
198204 }
199205
200206 /**
@@ -213,6 +219,7 @@ public AuthzeroTokenResolver(String issuer) {
213219 .apply (secret );
214220 this .issuer = issuer ;
215221 this .verifier = JWT .require (this .algorithm ).build ();
222+ this .objectMapper = new ObjectMapper ();
216223
217224 log .info ("The secret has been set to {}." , secret );
218225 }
@@ -370,16 +377,28 @@ public <T extends TokenPayload> String createToken(Duration expireAfter, String
370377 var fields = payloadClass .getDeclaredFields ();
371378
372379 for (var field : fields ) {
373- // Skip the fields which are annotated with ExcludeFromPayload
374- if (field .isAnnotationPresent (ExcludeFromPayload .class ))
375- continue ;
376-
377380 try {
378- field .setAccessible (true );
381+ var fieldName = field .getName ();
382+ // Skip the fields which are annotated with ExcludeFromPayload
383+ if (field .isAnnotationPresent (ExcludeFromPayload .class ))
384+ continue ;
385+
386+ Object invokeObj = payload ;
387+ var getter = payloadClass .getDeclaredMethod ("get" + fieldName .substring (0 , 1 ).toUpperCase () + fieldName .substring (1 ));
388+ if (field .isAnnotationPresent (TokenEnum .class )) {
389+ var tokenEnum = field .getAnnotation (TokenEnum .class );
390+ invokeObj = getter .invoke (payload );
391+ getter = field .getType ().getDeclaredMethod ("get" + tokenEnum .propertyName ().substring (0 , 1 ).toUpperCase () + tokenEnum .propertyName ().substring (1 ));
392+ }
393+
379394 // Build Claims
380- addClaim (builder , field . getName (), field . get ( payload ));
395+ addClaim (builder , fieldName , getter . invoke ( invokeObj ));
381396 } catch (IllegalAccessException e ) {
382397 log .error ("Cannot access field {}!" , field .getName ());
398+ } catch (NoSuchMethodException e ) {
399+ log .error ("Unable to find setter according to given field name." , e );
400+ } catch (InvocationTargetException e ) {
401+ log .info ("Cannot invoke method." , e );
383402 }
384403 }
385404
@@ -408,43 +427,66 @@ public DecodedJWT resolve(String token) {
408427 */
409428 @ Override
410429 public <T extends TokenPayload > T extract (String token , Class <T > targetType ) {
411- // Get claims from token.
412- var claims = resolve (token ).getClaims ();
413-
414430 try {
431+ // Get claims from token.
432+ var payloads = objectMapper .readValue (Base64Util .decode (resolve (token ).getPayload ()), new MapTypeReference ());
415433 // Get the no-argument constructor to create an instance.
416- T bean = targetType .getConstructor ().newInstance ();
434+ var bean = targetType .getConstructor ().newInstance ();
417435
418- var fields = targetType .getDeclaredFields ();
419- for (var field : fields ) {
420- // Ignore the field annotated with @ExcludeFromPayload.
421- if (field .isAnnotationPresent (ExcludeFromPayload .class ))
436+ for (var entry : payloads .entrySet ()) {
437+ // Jump all JWT pre-defined properties and the fields that are annotated to be excluded.
438+ if (PredefinedKeys .KEYS .contains (entry .getKey ()) || targetType .getDeclaredField (entry .getKey ()).isAnnotationPresent (ExcludeFromPayload .class ))
422439 continue ;
423440
424- // Get the name of this field.
425- var fieldName = field .getName ();
426-
427- // Prevent this class is annotated @Slf4j or added logger.
428- if ("log" .equalsIgnoreCase (fieldName ) || "logger" .equalsIgnoreCase (fieldName ))
429- continue ;
441+ var field = targetType .getDeclaredField (entry .getKey ());
442+ var setter = targetType .getDeclaredMethod ("set" + entry .getKey ().substring (0 , 1 ).toUpperCase () + entry .getKey ().substring (1 ), field .getType ());
443+ var fieldValue = entry .getValue ();
444+ if (field .isAnnotationPresent (TokenEnum .class )) {
445+ var annotation = field .getAnnotation (TokenEnum .class );
446+ var enumStaticLoader = field .getType ().getDeclaredMethod ("loadBy" + annotation .propertyName ().substring (0 , 1 ).toUpperCase () + annotation .propertyName ().substring (1 ), annotation .dataType ().getMappedClass ());
447+ fieldValue = enumStaticLoader .invoke (null , fieldValue );
448+ }
430449
431- // Get the value of this field.
432- var fieldValue = Optional .ofNullable (claims .get (fieldName ))
433- .map (claim -> claim .as (field .getType ()))
434- .orElse (null );
435- if (fieldValue != null ) {
436- // Set the field value by invoking the setter method.
437- var setter = targetType .getDeclaredMethod ("set" + fieldName .substring (0 , 1 ).toUpperCase () + fieldName .substring (1 ), fieldValue .getClass ());
450+ if (setter .canAccess (bean )) {
438451 setter .invoke (bean , fieldValue );
452+ } else {
453+ log .error ("Setter for field {} can't be accessed." , entry .getKey ());
439454 }
440455 }
441-
442456 return bean ;
443- } catch (NoSuchMethodException e ) {
444- log .error ("Unable to find a no-argument constructor declaration for class {}." , targetType .getCanonicalName ());
445- } catch (InstantiationException | IllegalAccessException | InvocationTargetException e ) {
446- log .error ("Unable to create a new instance of class {}." , targetType .getCanonicalName ());
457+ } catch (JsonProcessingException e ) {
458+ log .error ("Unable to read payload as a Map<String, Object>." , e );
459+ } catch (InvocationTargetException | InstantiationException | IllegalAccessException |
460+ NoSuchMethodException e ) {
461+ log .error ("Unable to load the constructor or setter." , e );
462+ } catch (NoSuchFieldException e ) {
463+ log .error ("Unable to load the field." , e );
464+ }
465+ return null ;
466+ }
467+
468+ /**
469+ * Re-generate a new token with the payload in the old one.
470+ *
471+ * @param oldToken the old token
472+ * @param expireAfter how long the new token can be valid for
473+ * @return re-generated token with the payload in the old one or
474+ * {@code null} if an {@link JsonProcessingException} occurred.
475+ */
476+ @ Override
477+ public String renew (String oldToken , Duration expireAfter ) {
478+ var resolved = resolve (oldToken );
479+
480+ try {
481+ var payload = objectMapper .readValue (Base64Util .decode (resolved .getPayload ()), ObjectNode .class );
482+ payload .remove (PredefinedKeys .KEYS );
483+
484+ var payloadMap = objectMapper .convertValue (payload , new MapTypeReference ());
485+ return createToken (expireAfter , resolved .getAudience ().get (0 ), resolved .getSubject (), payloadMap );
486+ } catch (JsonProcessingException e ) {
487+ log .error ("Cannot read payload content, error details:" , e );
447488 }
489+
448490 return null ;
449491 }
450492
@@ -509,4 +551,9 @@ public <T extends TokenPayload> String renew(String oldToken, Duration expireAft
509551 public <T extends TokenPayload > String renew (String oldToken , T payload ) {
510552 return renew (oldToken , Duration .ofMinutes (30 ), payload );
511553 }
554+
555+ private static class MapTypeReference extends TypeReference <Map <String , Object >> {
556+ MapTypeReference () {
557+ }
558+ }
512559}
0 commit comments