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 }