1
16 package io.jsonwebtoken.impl;
17
18 import com.fasterxml.jackson.databind.ObjectMapper;
19 import io.jsonwebtoken.ClaimJwtException;
20 import io.jsonwebtoken.Claims;
21 import io.jsonwebtoken.Clock;
22 import io.jsonwebtoken.CompressionCodec;
23 import io.jsonwebtoken.CompressionCodecResolver;
24 import io.jsonwebtoken.ExpiredJwtException;
25 import io.jsonwebtoken.Header;
26 import io.jsonwebtoken.IncorrectClaimException;
27 import io.jsonwebtoken.InvalidClaimException;
28 import io.jsonwebtoken.Jws;
29 import io.jsonwebtoken.JwsHeader;
30 import io.jsonwebtoken.Jwt;
31 import io.jsonwebtoken.JwtHandler;
32 import io.jsonwebtoken.JwtHandlerAdapter;
33 import io.jsonwebtoken.JwtParser;
34 import io.jsonwebtoken.MalformedJwtException;
35 import io.jsonwebtoken.MissingClaimException;
36 import io.jsonwebtoken.PrematureJwtException;
37 import io.jsonwebtoken.SignatureAlgorithm;
38 import io.jsonwebtoken.SignatureException;
39 import io.jsonwebtoken.SigningKeyResolver;
40 import io.jsonwebtoken.UnsupportedJwtException;
41 import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver;
42 import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator;
43 import io.jsonwebtoken.impl.crypto.JwtSignatureValidator;
44 import io.jsonwebtoken.lang.Assert;
45 import io.jsonwebtoken.lang.Objects;
46 import io.jsonwebtoken.lang.Strings;
47
48 import javax.crypto.spec.SecretKeySpec;
49 import java.io.IOException;
50 import java.security.Key;
51 import java.text.SimpleDateFormat;
52 import java.util.Date;
53 import java.util.Map;
54
55 @SuppressWarnings("unchecked")
56 public class DefaultJwtParser implements JwtParser {
57
58
59 private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
60 private static final int MILLISECONDS_PER_SECOND = 1000;
61
62 private ObjectMapper objectMapper = new ObjectMapper();
63
64 private byte[] keyBytes;
65
66 private Key key;
67
68 private SigningKeyResolver signingKeyResolver;
69
70 private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver();
71
72 Claims expectedClaims = new DefaultClaims();
73
74 private Clock clock = DefaultClock.INSTANCE;
75
76 private long allowedClockSkewMillis = 0;
77
78 @Override
79 public JwtParser requireIssuedAt(Date issuedAt) {
80 expectedClaims.setIssuedAt(issuedAt);
81 return this;
82 }
83
84 @Override
85 public JwtParser requireIssuer(String issuer) {
86 expectedClaims.setIssuer(issuer);
87 return this;
88 }
89
90 @Override
91 public JwtParser requireAudience(String audience) {
92 expectedClaims.setAudience(audience);
93 return this;
94 }
95
96 @Override
97 public JwtParser requireSubject(String subject) {
98 expectedClaims.setSubject(subject);
99 return this;
100 }
101
102 @Override
103 public JwtParser requireId(String id) {
104 expectedClaims.setId(id);
105 return this;
106 }
107
108 @Override
109 public JwtParser requireExpiration(Date expiration) {
110 expectedClaims.setExpiration(expiration);
111 return this;
112 }
113
114 @Override
115 public JwtParser requireNotBefore(Date notBefore) {
116 expectedClaims.setNotBefore(notBefore);
117 return this;
118 }
119
120 @Override
121 public JwtParser require(String claimName, Object value) {
122 Assert.hasText(claimName, "claim name cannot be null or empty.");
123 Assert.notNull(value, "The value cannot be null for claim name: " + claimName);
124 expectedClaims.put(claimName, value);
125 return this;
126 }
127
128 @Override
129 public JwtParser setClock(Clock clock) {
130 Assert.notNull(clock, "Clock instance cannot be null.");
131 this.clock = clock;
132 return this;
133 }
134
135 @Override
136 public JwtParser setAllowedClockSkewSeconds(long seconds) {
137 this.allowedClockSkewMillis = Math.max(0, seconds * MILLISECONDS_PER_SECOND);
138 return this;
139 }
140
141 @Override
142 public JwtParser setSigningKey(byte[] key) {
143 Assert.notEmpty(key, "signing key cannot be null or empty.");
144 this.keyBytes = key;
145 return this;
146 }
147
148 @Override
149 public JwtParser setSigningKey(String base64EncodedKeyBytes) {
150 Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty.");
151 this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes);
152 return this;
153 }
154
155 @Override
156 public JwtParser setSigningKey(Key key) {
157 Assert.notNull(key, "signing key cannot be null.");
158 this.key = key;
159 return this;
160 }
161
162 @Override
163 public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) {
164 Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null.");
165 this.signingKeyResolver = signingKeyResolver;
166 return this;
167 }
168
169 @Override
170 public JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) {
171 Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null.");
172 this.compressionCodecResolver = compressionCodecResolver;
173 return this;
174 }
175
176 @Override
177 public boolean isSigned(String jwt) {
178
179 if (jwt == null) {
180 return false;
181 }
182
183 int delimiterCount = 0;
184
185 for (int i = 0; i < jwt.length(); i++) {
186 char c = jwt.charAt(i);
187
188 if (delimiterCount == 2) {
189 return !Character.isWhitespace(c) && c != SEPARATOR_CHAR;
190 }
191
192 if (c == SEPARATOR_CHAR) {
193 delimiterCount++;
194 }
195 }
196
197 return false;
198 }
199
200 @Override
201 public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException {
202
203 Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
204
205 String base64UrlEncodedHeader = null;
206 String base64UrlEncodedPayload = null;
207 String base64UrlEncodedDigest = null;
208
209 int delimiterCount = 0;
210
211 StringBuilder sb = new StringBuilder(128);
212
213 for (char c : jwt.toCharArray()) {
214
215 if (c == SEPARATOR_CHAR) {
216
217 CharSequence tokenSeq = Strings.clean(sb);
218 String token = tokenSeq!=null?tokenSeq.toString():null;
219
220 if (delimiterCount == 0) {
221 base64UrlEncodedHeader = token;
222 } else if (delimiterCount == 1) {
223 base64UrlEncodedPayload = token;
224 }
225
226 delimiterCount++;
227 sb.setLength(0);
228 } else {
229 sb.append(c);
230 }
231 }
232
233 if (delimiterCount != 2) {
234 String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
235 throw new MalformedJwtException(msg);
236 }
237 if (sb.length() > 0) {
238 base64UrlEncodedDigest = sb.toString();
239 }
240
241 if (base64UrlEncodedPayload == null) {
242 throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
243 }
244
245
246 Header header = null;
247
248 CompressionCodec compressionCodec = null;
249
250 if (base64UrlEncodedHeader != null) {
251 String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
252 Map<String, Object> m = readValue(origValue);
253
254 if (base64UrlEncodedDigest != null) {
255 header = new DefaultJwsHeader(m);
256 } else {
257 header = new DefaultHeader(m);
258 }
259
260 compressionCodec = compressionCodecResolver.resolveCompressionCodec(header);
261 }
262
263
264 String payload;
265 if (compressionCodec != null) {
266 byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));
267 payload = new String(decompressed, Strings.UTF_8);
268 } else {
269 payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);
270 }
271
272 Claims claims = null;
273
274 if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') {
275 Map<String, Object> claimsMap = readValue(payload);
276 claims = new DefaultClaims(claimsMap);
277 }
278
279
280 if (base64UrlEncodedDigest != null) {
281
282 JwsHeader jwsHeader = (JwsHeader) header;
283
284 SignatureAlgorithm algorithm = null;
285
286 if (header != null) {
287 String alg = jwsHeader.getAlgorithm();
288 if (Strings.hasText(alg)) {
289 algorithm = SignatureAlgorithm.forName(alg);
290 }
291 }
292
293 if (algorithm == null || algorithm == SignatureAlgorithm.NONE) {
294
295 String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " +
296 "algorithm.";
297 throw new MalformedJwtException(msg);
298 }
299
300 if (key != null && keyBytes != null) {
301 throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either.");
302 } else if ((key != null || keyBytes != null) && signingKeyResolver != null) {
303 String object = key != null ? "a key object" : "key bytes";
304 throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either.");
305 }
306
307
308 Key key = this.key;
309
310 if (key == null) {
311
312 byte[] keyBytes = this.keyBytes;
313
314 if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) {
315 if (claims != null) {
316 key = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
317 } else {
318 key = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
319 }
320 }
321
322 if (!Objects.isEmpty(keyBytes)) {
323
324 Assert.isTrue(algorithm.isHmac(),
325 "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.");
326
327 key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
328 }
329 }
330
331 Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed.");
332
333
334 String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;
335
336 JwtSignatureValidator validator;
337 try {
338 validator = createSignatureValidator(algorithm, key);
339 } catch (IllegalArgumentException e) {
340 String algName = algorithm.getValue();
341 String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " +
342 "algorithm, but the specified signing key of type " + key.getClass().getName() +
343 " may not be used to validate " + algName + " signatures. Because the specified " +
344 "signing key reflects a specific and expected algorithm, and the JWT does not reflect " +
345 "this algorithm, it is likely that the JWT was not expected and therefore should not be " +
346 "trusted. Another possibility is that the parser was configured with the incorrect " +
347 "signing key, but this cannot be assumed for security reasons.";
348 throw new UnsupportedJwtException(msg, e);
349 }
350
351 if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) {
352 String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " +
353 "asserted and should not be trusted.";
354 throw new SignatureException(msg);
355 }
356 }
357
358 final boolean allowSkew = this.allowedClockSkewMillis > 0;
359
360
361 if (claims != null) {
362
363 SimpleDateFormat sdf;
364
365 final Date now = this.clock.now();
366 long nowTime = now.getTime();
367
368
369
370 Date exp = claims.getExpiration();
371 if (exp != null) {
372
373 long maxTime = nowTime - this.allowedClockSkewMillis;
374 Date max = allowSkew ? new Date(maxTime) : now;
375 if (max.after(exp)) {
376 sdf = new SimpleDateFormat(ISO_8601_FORMAT);
377 String expVal = sdf.format(exp);
378 String nowVal = sdf.format(now);
379
380 long differenceMillis = maxTime - exp.getTime();
381
382 String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " +
383 differenceMillis + " milliseconds. Allowed clock skew: " +
384 this.allowedClockSkewMillis + " milliseconds.";
385 throw new ExpiredJwtException(header, claims, msg);
386 }
387 }
388
389
390
391 Date nbf = claims.getNotBefore();
392 if (nbf != null) {
393
394 long minTime = nowTime + this.allowedClockSkewMillis;
395 Date min = allowSkew ? new Date(minTime) : now;
396 if (min.before(nbf)) {
397 sdf = new SimpleDateFormat(ISO_8601_FORMAT);
398 String nbfVal = sdf.format(nbf);
399 String nowVal = sdf.format(now);
400
401 long differenceMillis = nbf.getTime() - minTime;
402
403 String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal +
404 ", a difference of " +
405 differenceMillis + " milliseconds. Allowed clock skew: " +
406 this.allowedClockSkewMillis + " milliseconds.";
407 throw new PrematureJwtException(header, claims, msg);
408 }
409 }
410
411 validateExpectedClaims(header, claims);
412 }
413
414 Object body = claims != null ? claims : payload;
415
416 if (base64UrlEncodedDigest != null) {
417 return new DefaultJws<Object>((JwsHeader) header, body, base64UrlEncodedDigest);
418 } else {
419 return new DefaultJwt<Object>(header, body);
420 }
421 }
422
423 private void validateExpectedClaims(Header header, Claims claims) {
424 for (String expectedClaimName : expectedClaims.keySet()) {
425
426 Object expectedClaimValue = expectedClaims.get(expectedClaimName);
427 Object actualClaimValue = claims.get(expectedClaimName);
428
429 if (
430 Claims.ISSUED_AT.equals(expectedClaimName) ||
431 Claims.EXPIRATION.equals(expectedClaimName) ||
432 Claims.NOT_BEFORE.equals(expectedClaimName)
433 ) {
434 expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class);
435 actualClaimValue = claims.get(expectedClaimName, Date.class);
436 } else if (
437 expectedClaimValue instanceof Date &&
438 actualClaimValue != null &&
439 actualClaimValue instanceof Long
440 ) {
441 actualClaimValue = new Date((Long)actualClaimValue);
442 }
443
444 InvalidClaimException invalidClaimException = null;
445
446 if (actualClaimValue == null) {
447 String msg = String.format(
448 ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE,
449 expectedClaimName, expectedClaimValue
450 );
451 invalidClaimException = new MissingClaimException(header, claims, msg);
452 } else if (!expectedClaimValue.equals(actualClaimValue)) {
453 String msg = String.format(
454 ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE,
455 expectedClaimName, expectedClaimValue, actualClaimValue
456 );
457 invalidClaimException = new IncorrectClaimException(header, claims, msg);
458 }
459
460 if (invalidClaimException != null) {
461 invalidClaimException.setClaimName(expectedClaimName);
462 invalidClaimException.setClaimValue(expectedClaimValue);
463 throw invalidClaimException;
464 }
465 }
466 }
467
468
471 protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) {
472 return new DefaultJwtSignatureValidator(alg, key);
473 }
474
475 @Override
476 public <T> T parse(String compact, JwtHandler<T> handler)
477 throws ExpiredJwtException, MalformedJwtException, SignatureException {
478 Assert.notNull(handler, "JwtHandler argument cannot be null.");
479 Assert.hasText(compact, "JWT String argument cannot be null or empty.");
480
481 Jwt jwt = parse(compact);
482
483 if (jwt instanceof Jws) {
484 Jws jws = (Jws) jwt;
485 Object body = jws.getBody();
486 if (body instanceof Claims) {
487 return handler.onClaimsJws((Jws<Claims>) jws);
488 } else {
489 return handler.onPlaintextJws((Jws<String>) jws);
490 }
491 } else {
492 Object body = jwt.getBody();
493 if (body instanceof Claims) {
494 return handler.onClaimsJwt((Jwt<Header, Claims>) jwt);
495 } else {
496 return handler.onPlaintextJwt((Jwt<Header, String>) jwt);
497 }
498 }
499 }
500
501 @Override
502 public Jwt<Header, String> parsePlaintextJwt(String plaintextJwt) {
503 return parse(plaintextJwt, new JwtHandlerAdapter<Jwt<Header, String>>() {
504 @Override
505 public Jwt<Header, String> onPlaintextJwt(Jwt<Header, String> jwt) {
506 return jwt;
507 }
508 });
509 }
510
511 @Override
512 public Jwt<Header, Claims> parseClaimsJwt(String claimsJwt) {
513 try {
514 return parse(claimsJwt, new JwtHandlerAdapter<Jwt<Header, Claims>>() {
515 @Override
516 public Jwt<Header, Claims> onClaimsJwt(Jwt<Header, Claims> jwt) {
517 return jwt;
518 }
519 });
520 } catch (IllegalArgumentException iae) {
521 throw new UnsupportedJwtException("Signed JWSs are not supported.", iae);
522 }
523 }
524
525 @Override
526 public Jws<String> parsePlaintextJws(String plaintextJws) {
527 try {
528 return parse(plaintextJws, new JwtHandlerAdapter<Jws<String>>() {
529 @Override
530 public Jws<String> onPlaintextJws(Jws<String> jws) {
531 return jws;
532 }
533 });
534 } catch (IllegalArgumentException iae) {
535 throw new UnsupportedJwtException("Signed JWSs are not supported.", iae);
536 }
537 }
538
539 @Override
540 public Jws<Claims> parseClaimsJws(String claimsJws) {
541 return parse(claimsJws, new JwtHandlerAdapter<Jws<Claims>>() {
542 @Override
543 public Jws<Claims> onClaimsJws(Jws<Claims> jws) {
544 return jws;
545 }
546 });
547 }
548
549 @SuppressWarnings("unchecked")
550 protected Map<String, Object> readValue(String val) {
551 try {
552 return objectMapper.readValue(val, Map.class);
553 } catch (IOException e) {
554 throw new MalformedJwtException("Unable to read JSON value: " + val, e);
555 }
556 }
557 }
558