1 module jwt.jwt;
2 
3 import std.json;
4 import std.base64;
5 import std.stdio;
6 import std.conv;
7 import std.string;
8 import std.datetime;
9 import std.exception;
10 import std.array : split;
11 import std.algorithm : count;
12 
13 import jwt.algorithms;
14 import jwt.exceptions;
15 
16 private class Component {
17 
18     abstract @property string json();
19 
20     @property string base64() {
21 
22         ubyte[] data = cast(ubyte[])this.json;
23 
24         return URLSafeBase64.encode(data);
25 
26     }
27 
28 }
29 
30 private class Header : Component {
31 
32 public:
33     JWTAlgorithm alg;
34     string typ;
35 
36     this(in JWTAlgorithm alg, in string typ) {
37 
38         this.alg = alg;
39         this.typ = typ;
40     }
41 
42     this(in JSONValue headers) {
43 
44         try {
45 
46             this.alg = to!(JWTAlgorithm)(toUpper(headers["alg"].str()));
47 
48         } catch (Exception e) {
49 
50             throw new UnsupportedAlgorithmException(alg ~ " algorithm is not supported!");
51 
52         }
53 
54         this.typ = headers["typ"].str();
55 
56     }
57 
58     @property override string json() {
59 
60         JSONValue headers = ["alg": cast(string)this.alg, "typ": this.typ];
61 
62         return headers.toString();
63 
64     }
65 
66 }
67 
68 /**
69 * represents the claims component of a JWT
70 */
71 private class Claims : Component {
72 private:
73     JSONValue data;
74 
75     this(in JSONValue claims) {
76 
77         this.data = claims;
78 
79     }
80 
81 public:
82 
83     this() {
84 
85         this.data = JSONValue(["iat": JSONValue(Clock.currTime.toUnixTime())]);
86 
87     }
88 
89     void set(T)(string name, T data) {
90         this.data.object[name] = JSONValue(data);
91     }
92 
93     /**
94     * Params:
95     *       name = the name of the claim
96     * Returns: returns a string representation of the claim if it exists and is a string or an empty string if doesn't exist or is not a string
97     */
98     string get(string name) {
99 
100         try {
101 
102             return this.data[name].str();
103 
104         } catch (JSONException e) {
105 
106             return string.init;
107 
108         }
109 
110     }
111 
112     /**
113     * Params:
114     *       name = the name of the claim
115     * Returns: an array of JSONValue
116     */
117     JSONValue[] getArray(string name) {
118 
119         try {
120 
121             return this.data[name].array();
122 
123         } catch (JSONException e) {
124 
125             return JSONValue.Store.array.init;
126 
127         }
128 
129     }
130 
131 
132     /**
133     * Params:
134     *       name = the name of the claim
135     * Returns: a JSONValue
136     */
137     JSONValue[string] getObject(string name) {
138 
139         try {
140 
141             return this.data[name].object();
142 
143         } catch (JSONException e) {
144 
145             return JSONValue.Store.object.init;
146 
147         }
148 
149     }
150 
151     /**
152     * Params:
153     *       name = the name of the claim
154     * Returns: returns a long representation of the claim if it exists and is an
155     *          integer or the initial value for long if doesn't exist or is not an integer
156     */
157     long getInt(string name) {
158 
159         try {
160 
161             return this.data[name].integer();
162 
163         } catch (JSONException e) {
164 
165             return long.init;
166 
167         }
168 
169     }
170 
171     /**
172     * Params:
173     *       name = the name of the claim
174     * Returns: returns a double representation of the claim if it exists and is a
175     *          double or the initial value for double if doesn't exist or is not a double
176     */
177     double getDouble(string name) {
178 
179         try {
180 
181             return this.data[name].floating();
182 
183         } catch (JSONException e) {
184 
185             return double.init;
186 
187         }
188 
189     }
190 
191     /**
192     * Params:
193     *       name = the name of the claim
194     * Returns: returns a boolean representation of the claim if it exists and is a
195     *          boolean or the initial value for bool if doesn't exist or is not a boolean
196     */
197     bool getBool(string name) {
198 
199         try {
200 
201             return this.data[name].type == JSON_TYPE.TRUE;
202 
203         } catch (JSONException e) {
204 
205             return bool.init;
206 
207         }
208 
209     }
210 
211     /**
212     * Params:
213     *       name = the name of the claim
214     * Returns: returns a boolean value if the claim exists and is null or
215     *          the initial value for bool it it doesn't exist or is not null
216     */
217     bool isNull(string name) {
218 
219         try {
220 
221             return this.data[name].isNull();
222 
223         } catch (JSONException) {
224 
225             return bool.init;
226 
227         }
228 
229     }
230 
231     @property void iss(string s) {
232         this.data.object["iss"] = s;
233     }
234 
235 
236     @property string iss() {
237 
238         try {
239 
240             return this.data["iss"].str();
241 
242         } catch (JSONException e) {
243 
244             return "";
245 
246         }
247 
248     }
249 
250     @property void sub(string s) {
251         this.data.object["sub"] = s;
252     }
253 
254     @property string sub() {
255 
256         try {
257 
258             return this.data["sub"].str();
259 
260         } catch (JSONException e) {
261 
262             return "";
263 
264         }
265 
266     }
267 
268     @property void aud(string s) {
269         this.data.object["aud"] = s;
270     }
271 
272     @property string aud() {
273 
274         try {
275 
276             return this.data["aud"].str();
277 
278         } catch (JSONException e) {
279 
280             return "";
281 
282         }
283 
284     }
285 
286     @property void exp(long n) {
287         this.data.object["exp"] = n;
288     }
289 
290     @property long exp() {
291 
292         try {
293 
294             return this.data["exp"].integer;
295 
296         } catch (JSONException) {
297 
298             return 0;
299 
300         }
301 
302     }
303 
304     @property void nbf(long n) {
305         this.data.object["nbf"] = n;
306     }
307 
308     @property long nbf() {
309 
310         try {
311 
312             return this.data["nbf"].integer;
313 
314         } catch (JSONException) {
315 
316             return 0;
317 
318         }
319 
320     }
321 
322     @property void iat(long n) {
323         this.data.object["iat"] = n;
324     }
325 
326     @property long iat() {
327 
328         try {
329 
330             return this.data["iat"].integer;
331 
332         } catch (JSONException) {
333 
334             return 0;
335 
336         }
337 
338     }
339 
340     @property void jit(string s) {
341         this.data.object["jit"] = s;
342     }
343 
344     @property string jit() {
345 
346         try {
347 
348             return this.data["jit"].str();
349 
350         } catch(JSONException e) {
351 
352             return "";
353 
354         }
355 
356     }
357 
358     /**
359     * gives json encoded claims
360     * Returns: json encoded claims
361     */
362     @property override string json() {
363 
364         return this.data.toString();
365 
366     }
367 
368 }
369 
370 /**
371 * represents a token
372 */
373 class Token {
374 
375 private:
376     Claims _claims;
377     Header _header;
378 
379     this(Claims claims, Header header) {
380         this._claims = claims;
381         this._header = header;
382     }
383 
384     @property string data() {
385         return this.header.base64 ~ "." ~ this.claims.base64;
386     }
387 
388 
389 public:
390 
391     this(in JWTAlgorithm alg, in string typ = "JWT") {
392 
393         this._claims = new Claims();
394 
395         this._header = new Header(alg, typ);
396 
397     }
398 
399     @property Claims claims() {
400         return this._claims;
401     }
402 
403     @property Header header() {
404         return this._header;
405     }
406 
407     /**
408     * used to get the signature of the token
409     * Parmas:
410     *       secret = the secret key used to sign the token
411     * Returns: the signature of the token
412     */
413     string signature(string secret) {
414 
415         return sign(secret, this.data, this.header.alg);
416 
417     }
418 
419     /**
420     * encodes the token
421     * Params:
422     *       secret = the secret key used to sign the token
423     *Returns: base64 representation of the token including signature
424     */
425     string encode(string secret) {
426 
427         if ((this.claims.exp != ulong.init && this.claims.iat != ulong.init) && this.claims.exp < this.claims.iat) {
428             throw new ExpiredException("Token has already expired");
429         }
430 
431         if ((this.claims.exp != ulong.init && this.claims.nbf != ulong.init) && this.claims.exp < this.claims.nbf) {
432             throw new ExpiresBeforeValidException("Token will expire before it becomes valid");
433         }
434 
435         return this.data ~ "." ~ this.signature(secret);
436 
437     }
438     ///
439     unittest {
440 
441         Token token = new Token(JWTAlgorithm.HS512);
442 
443         long now = Clock.currTime.toUnixTime();
444 
445         string secret = "super_secret";
446 
447         token.claims.exp = now - 3600;
448 
449         assertThrown!ExpiredException(token.encode(secret));
450 
451         token.claims.exp = now + 3600;
452 
453         token.claims.nbf = now + 7200;
454 
455         assertThrown!ExpiresBeforeValidException(token.encode(secret));
456 
457     }
458 
459     /**
460     * overload of the encode(string secret) function to simplify encoding of token without algorithm none
461     * Returns: base64 representation of the token
462     */
463     string encode() {
464         assert(this.header.alg == JWTAlgorithm.NONE);
465         return this.encode("");
466     }
467 
468 }
469 
470 private Token decode(string encodedToken) {
471 
472     string[] tokenParts = split(encodedToken, ".");
473 
474     if(tokenParts.length != 3) {
475         throw new MalformedToken("Malformed Token");
476     }
477 
478     string component = tokenParts[0];
479 
480     string jsonComponent = cast(string)URLSafeBase64.decode(component);
481 
482     JSONValue parsedComponent = parseJSON(jsonComponent);
483 
484     Header header = new Header(parsedComponent);
485 
486     component = tokenParts[1];
487 
488     jsonComponent = cast(string)URLSafeBase64.decode(component);
489 
490     parsedComponent = parseJSON(jsonComponent);
491 
492     Claims claims = new Claims(parsedComponent);
493 
494     return new Token(claims, header);
495 
496 }
497 
498 /**
499 * verifies the tokens is valid, using the algorithm given instead of the alg field in the claims
500 * Params:
501 *       encodedToken = the encoded token
502 *       secret = the secret key used to sign the token
503 *       alg = the algorithm to be used to verify the token
504 * Returns: a decoded Token
505 */
506 Token verify(string encodedToken, string secret, JWTAlgorithm[] algs) {
507 
508     Token token = decode(encodedToken);
509 
510     long now = Clock.currTime.toUnixTime();
511 
512     bool algorithmAllowed = false;
513 
514     foreach(index, alg; algs) {
515 
516         if(token.header.alg == alg) {
517             algorithmAllowed = true;
518         }
519 
520     }
521 
522     if (!algorithmAllowed) {
523         throw new InvalidAlgorithmException("Algorithm " ~ token.header.alg ~ " is not in the allowed algorithms field");
524     }
525 
526     string signature = split(encodedToken, ".")[2];
527 
528     if (signature != token.signature(secret)) {
529         throw new InvalidSignatureException("Signature Match Failed");
530     }
531 
532     if (token.header.alg == JWTAlgorithm.NONE) {
533         throw new VerifyException("Algorithm set to none while secret is provided");
534     }
535 
536     if (token.claims.exp != ulong.init && token.claims.exp < now) {
537         throw new ExpiredException("Token has expired");
538     }
539 
540     if (token.claims.nbf != ulong.init && token.claims.nbf > now) {
541         throw new NotBeforeException("Token is not valid yet");
542     }
543 
544     return token;
545 
546 }
547 
548 unittest {
549 
550     string secret = "super_secret";
551 
552     long now = Clock.currTime.toUnixTime();
553 
554     Token token = new Token(JWTAlgorithm.HS512);
555 
556     token.claims.nbf = now + (60 * 60);
557 
558     string encodedToken = token.encode(secret);
559 
560     assertThrown!NotBeforeException(verify(encodedToken, secret, [JWTAlgorithm.HS512]));
561 
562     token = new Token(JWTAlgorithm.HS512);
563 
564     token.claims.iat = now - 3600;
565 
566     token.claims.exp = now - 60;
567 
568     encodedToken = token.encode(secret);
569 
570     assertThrown!ExpiredException(verify(encodedToken, secret, [JWTAlgorithm.HS512]));
571 
572     token = new Token(JWTAlgorithm.NONE);
573 
574     encodedToken = token.encode(secret);
575 
576     assertThrown!VerifyException(verify(encodedToken, secret, [JWTAlgorithm.HS512]));
577 
578     token = new Token(JWTAlgorithm.HS512);
579 
580     encodedToken = token.encode(secret) ~ "we_are";
581 
582     assertThrown!InvalidSignatureException(verify(encodedToken, secret, [JWTAlgorithm.HS512]));
583 
584     token = new Token(JWTAlgorithm.HS512);
585 
586     encodedToken = token.encode(secret);
587 
588     assertThrown!InvalidAlgorithmException(verify(encodedToken, secret, [JWTAlgorithm.HS256, JWTAlgorithm.HS384]));
589 
590 }
591 
592 /**
593 * verifies the tokens is valid, used in case the token was signed with "none" as algorithm
594 * Params:
595 *       encodedToken = the encoded token
596 * Returns: a decoded Token
597 */
598 Token verify(string encodedToken) {
599 
600     Token token = decode(encodedToken);
601 
602     long now = Clock.currTime.toUnixTime();
603 
604     if (token.claims.exp != ulong.init && token.claims.exp < now) {
605         throw new ExpiredException("Token has expired");
606     }
607 
608     if (token.claims.nbf != ulong.init && token.claims.nbf > now) {
609         throw new NotBeforeException("Token is not valid yet");
610     }
611 
612     return token;
613 
614 }
615 ///
616 unittest {
617 
618     long now = Clock.currTime.toUnixTime();
619 
620     Token token = new Token(JWTAlgorithm.NONE);
621 
622     token.claims.nbf = now + (60 * 60);
623 
624     string encodedToken = token.encode();
625 
626     assertThrown!NotBeforeException(verify(encodedToken));
627 
628     token = new Token(JWTAlgorithm.NONE);
629 
630     token.claims.iat = now - 3600;
631 
632     token.claims.exp = now - 60;
633 
634     encodedToken = token.encode();
635 
636     assertThrown!ExpiredException(token = verify(encodedToken));
637 
638 }