1 /*
2  * Copyright (C) 2014 jsonwebtoken.io
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

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     //don't need millis since JWT date fields are only second granularity:
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         // =============== Header =================
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         // =============== Body =================
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) == '}') { //likely to be json, parse it:
275             Map<String, Object> claimsMap = readValue(payload);
276             claims = new DefaultClaims(claimsMap);
277         }
278
279         // =============== Signature =================
280         if (base64UrlEncodedDigest != null) { //it is signed - validate the signature
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                 //it is plaintext, but it has a signature.  This is invalid:
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             //digitally signed, let's assert the signature:
308             Key key = this.key;
309
310             if (key == null) { //fall back to keyBytes
311
312                 byte[] keyBytes = this.keyBytes;
313
314                 if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
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             //re-create the jwt part without the signature.  This is what needs to be signed for verification:
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         //since 0.3:
361         if (claims != null) {
362
363             SimpleDateFormat sdf;
364
365             final Date now = this.clock.now();
366             long nowTime = now.getTime();
367
368             //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4
369             //token MUST NOT be accepted on or after any specified exp time:
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             //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.5
390             //token MUST NOT be accepted before any specified nbf time:
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     /*
469      * @since 0.5 mostly to allow testing overrides
470      */

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