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 }