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