001package com.nimbusds.jwt; 002 003 004import java.text.ParseException; 005import java.util.*; 006 007import net.jcip.annotations.Immutable; 008 009import net.minidev.json.JSONArray; 010import net.minidev.json.JSONObject; 011 012import com.nimbusds.jose.util.JSONObjectUtils; 013 014 015/** 016 * JSON Web Token (JWT) claims set. This class is immutable. 017 * 018 * <p>Supports all {@link #getRegisteredNames()} registered claims} of the JWT 019 * specification: 020 * 021 * <ul> 022 * <li>iss - Issuer 023 * <li>sub - Subject 024 * <li>aud - Audience 025 * <li>exp - Expiration Time 026 * <li>nbf - Not Before 027 * <li>iat - Issued At 028 * <li>jti - JWT ID 029 * </ul> 030 * 031 * <p>The set may also contain custom claims; these will be serialised and 032 * parsed along the registered ones. 033 * 034 * <p>Example JWT claims set: 035 * 036 * <pre> 037 * { 038 * "sub" : "joe", 039 * "exp" : 1300819380, 040 * "http://example.com/is_root" : true 041 * } 042 * </pre> 043 * 044 * <p>Example usage: 045 * 046 * <pre> 047 * JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 048 * .subject("joe") 049 * .expirationDate(new Date(1300819380 * 1000l) 050 * .claim("http://example.com/is_root", true) 051 * .build(); 052 * </pre> 053 * 054 * @author Vladimir Dzhuvinov 055 * @author Justin Richer 056 * @version 2015-08-22 057 */ 058@Immutable 059public final class JWTClaimsSet { 060 061 062 private static final String ISSUER_CLAIM = "iss"; 063 private static final String SUBJECT_CLAIM = "sub"; 064 private static final String AUDIENCE_CLAIM = "aud"; 065 private static final String EXPIRATION_TIME_CLAIM = "exp"; 066 private static final String NOT_BEFORE_CLAIM = "nbf"; 067 private static final String ISSUED_AT_CLAIM = "iat"; 068 private static final String JWT_ID_CLAIM = "jti"; 069 070 071 /** 072 * The registered claim names. 073 */ 074 private static final Set<String> REGISTERED_CLAIM_NAMES; 075 076 077 /** 078 * Initialises the registered claim name set. 079 */ 080 static { 081 Set<String> n = new HashSet<>(); 082 083 n.add(ISSUER_CLAIM); 084 n.add(SUBJECT_CLAIM); 085 n.add(AUDIENCE_CLAIM); 086 n.add(EXPIRATION_TIME_CLAIM); 087 n.add(NOT_BEFORE_CLAIM); 088 n.add(ISSUED_AT_CLAIM); 089 n.add(JWT_ID_CLAIM); 090 091 REGISTERED_CLAIM_NAMES = Collections.unmodifiableSet(n); 092 } 093 094 095 /** 096 * Builder for constructing JSON Web Token (JWT) claims sets. 097 * 098 * <p>Example usage: 099 * 100 * <pre> 101 * JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 102 * .subject("joe") 103 * .expirationDate(new Date(1300819380 * 1000l) 104 * .claim("http://example.com/is_root", true) 105 * .build(); 106 * </pre> 107 */ 108 public static class Builder { 109 110 111 /** 112 * The claims. 113 */ 114 private final Map<String,Object> claims = new LinkedHashMap<>(); 115 116 117 /** 118 * Creates a new builder. 119 */ 120 public Builder() { 121 122 // Nothing to do 123 } 124 125 126 /** 127 * Creates a new builder with the claims from the specified 128 * set. 129 * 130 * @param jwtClaimsSet The JWT claims set to use. Must not be 131 * {@code null}. 132 */ 133 public Builder(final JWTClaimsSet jwtClaimsSet) { 134 135 claims.putAll(jwtClaimsSet.claims); 136 } 137 138 139 /** 140 * Sets the issuer ({@code iss}) claim. 141 * 142 * @param iss The issuer claim, {@code null} if not specified. 143 * 144 * @return This builder. 145 */ 146 public Builder issuer(final String iss) { 147 148 claims.put(ISSUER_CLAIM, iss); 149 return this; 150 } 151 152 153 /** 154 * Sets the subject ({@code sub}) claim. 155 * 156 * @param sub The subject claim, {@code null} if not specified. 157 * 158 * @return This builder. 159 */ 160 public Builder subject(final String sub) { 161 162 claims.put(SUBJECT_CLAIM, sub); 163 return this; 164 } 165 166 167 /** 168 * Sets the audience ({@code aud}) claim. 169 * 170 * @param aud The audience claim, {@code null} if not 171 * specified. 172 * 173 * @return This builder. 174 */ 175 public Builder audience(final List<String> aud) { 176 177 claims.put(AUDIENCE_CLAIM, aud); 178 return this; 179 } 180 181 182 /** 183 * Sets a single-valued audience ({@code aud}) claim. 184 * 185 * @param aud The audience claim, {@code null} if not 186 * specified. 187 * 188 * @return This builder. 189 */ 190 public Builder audience(final String aud) { 191 192 if (aud == null) { 193 claims.put(AUDIENCE_CLAIM, null); 194 } else { 195 claims.put(AUDIENCE_CLAIM, Arrays.asList(aud)); 196 } 197 return this; 198 } 199 200 201 /** 202 * Sets the expiration time ({@code exp}) claim. 203 * 204 * @param exp The expiration time, {@code null} if not 205 * specified. 206 * 207 * @return This builder. 208 */ 209 public Builder expirationTime(final Date exp) { 210 211 claims.put(EXPIRATION_TIME_CLAIM, exp); 212 return this; 213 } 214 215 216 /** 217 * Sets the not-before ({@code nbf}) claim. 218 * 219 * @param nbf The not-before claim, {@code null} if not 220 * specified. 221 * 222 * @return This builder. 223 */ 224 public Builder notBeforeTime(final Date nbf) { 225 226 claims.put(NOT_BEFORE_CLAIM, nbf); 227 return this; 228 } 229 230 231 /** 232 * Sets the issued-at ({@code iat}) claim. 233 * 234 * @param iat The issued-at claim, {@code null} if not 235 * specified. 236 * 237 * @return This builder. 238 */ 239 public Builder issueTime(final Date iat) { 240 241 claims.put(ISSUED_AT_CLAIM, iat); 242 return this; 243 } 244 245 246 /** 247 * Sets the JWT ID ({@code jti}) claim. 248 * 249 * @param jti The JWT ID claim, {@code null} if not specified. 250 * 251 * @return This builder. 252 */ 253 public Builder jwtID(final String jti) { 254 255 claims.put(JWT_ID_CLAIM, jti); 256 return this; 257 } 258 259 260 /** 261 * Sets the specified claim (registered or custom). 262 * 263 * @param name The name of the claim to set. Must not be 264 * {@code null}. 265 * @param value The value of the claim to set, {@code null} if 266 * not specified. Should map to a JSON entity. 267 * 268 * @return This builder. 269 */ 270 public Builder claim(final String name, final Object value) { 271 272 claims.put(name, value); 273 return this; 274 } 275 276 277 /** 278 * Builds a new JWT claims set. 279 * 280 * @return The JWT claims set. 281 */ 282 public JWTClaimsSet build() { 283 284 return new JWTClaimsSet(claims); 285 } 286 } 287 288 289 /** 290 * The claims map. 291 */ 292 private final Map<String,Object> claims = new LinkedHashMap<>(); 293 294 295 /** 296 * Creates a new JWT claims set. 297 * 298 * @param claims The JWT claims set as a map. Must not be {@code null}. 299 */ 300 private JWTClaimsSet(final Map<String,Object> claims) { 301 302 this.claims.putAll(claims); 303 } 304 305 306 /** 307 * Gets the registered JWT claim names. 308 * 309 * @return The registered claim names, as a unmodifiable set. 310 */ 311 public static Set<String> getRegisteredNames() { 312 313 return REGISTERED_CLAIM_NAMES; 314 } 315 316 317 /** 318 * Gets the issuer ({@code iss}) claim. 319 * 320 * @return The issuer claim, {@code null} if not specified. 321 */ 322 public String getIssuer() { 323 324 try { 325 return getStringClaim(ISSUER_CLAIM); 326 } catch (ParseException e) { 327 return null; 328 } 329 } 330 331 332 /** 333 * Gets the subject ({@code sub}) claim. 334 * 335 * @return The subject claim, {@code null} if not specified. 336 */ 337 public String getSubject() { 338 339 try { 340 return getStringClaim(SUBJECT_CLAIM); 341 } catch (ParseException e) { 342 return null; 343 } 344 } 345 346 347 /** 348 * Gets the audience ({@code aud}) clam. 349 * 350 * @return The audience claim, {@code null} if not specified. 351 */ 352 public List<String> getAudience() { 353 354 List<String> aud; 355 try { 356 aud = getStringListClaim(AUDIENCE_CLAIM); 357 } catch (ParseException e) { 358 return null; 359 } 360 return aud != null ? Collections.unmodifiableList(aud) : null; 361 } 362 363 364 /** 365 * Gets the expiration time ({@code exp}) claim. 366 * 367 * @return The expiration time, {@code null} if not specified. 368 */ 369 public Date getExpirationTime() { 370 371 try { 372 return getDateClaim(EXPIRATION_TIME_CLAIM); 373 } catch (ParseException e) { 374 return null; 375 } 376 } 377 378 379 /** 380 * Gets the not-before ({@code nbf}) claim. 381 * 382 * @return The not-before claim, {@code null} if not specified. 383 */ 384 public Date getNotBeforeTime() { 385 386 try { 387 return getDateClaim(NOT_BEFORE_CLAIM); 388 } catch (ParseException e) { 389 return null; 390 } 391 } 392 393 394 /** 395 * Gets the issued-at ({@code iat}) claim. 396 * 397 * @return The issued-at claim, {@code null} if not specified. 398 */ 399 public Date getIssueTime() { 400 401 try { 402 return getDateClaim(ISSUED_AT_CLAIM); 403 } catch (ParseException e) { 404 return null; 405 } 406 } 407 408 409 /** 410 * Gets the JWT ID ({@code jti}) claim. 411 * 412 * @return The JWT ID claim, {@code null} if not specified. 413 */ 414 public String getJWTID() { 415 416 try { 417 return getStringClaim(JWT_ID_CLAIM); 418 } catch (ParseException e) { 419 return null; 420 } 421 } 422 423 424 /** 425 * Gets the specified claim (registered or custom). 426 * 427 * @param name The name of the claim. Must not be {@code null}. 428 * 429 * @return The value of the claim, {@code null} if not specified. 430 */ 431 public Object getClaim(final String name) { 432 433 return claims.get(name); 434 } 435 436 437 /** 438 * Gets the specified claim (registered or custom) as 439 * {@link java.lang.String}. 440 * 441 * @param name The name of the claim. Must not be {@code null}. 442 * 443 * @return The value of the claim, {@code null} if not specified. 444 * 445 * @throws ParseException If the claim value is not of the required 446 * type. 447 */ 448 public String getStringClaim(final String name) 449 throws ParseException { 450 451 Object value = getClaim(name); 452 453 if (value == null || value instanceof String) { 454 return (String)value; 455 } else { 456 throw new ParseException("The \"" + name + "\" claim is not a String", 0); 457 } 458 } 459 460 461 /** 462 * Gets the specified claims (registered or custom) as a 463 * {@link java.lang.String} array. 464 * 465 * @param name The name of the claim. Must not be {@code null}. 466 * 467 * @return The value of the claim, {@code null} if not specified. 468 * 469 * @throws ParseException If the claim value is not of the required 470 * type. 471 */ 472 public String[] getStringArrayClaim(final String name) 473 throws ParseException { 474 475 Object value = getClaim(name); 476 477 if (value == null) { 478 return null; 479 } 480 481 List<?> list; 482 483 try { 484 list = (List)getClaim(name); 485 486 } catch (ClassCastException e) { 487 throw new ParseException("The \"" + name + "\" claim is not a list / JSON array", 0); 488 } 489 490 String[] stringArray = new String[list.size()]; 491 492 for (int i=0; i < stringArray.length; i++) { 493 494 try { 495 stringArray[i] = (String)list.get(i); 496 } catch (ClassCastException e) { 497 throw new ParseException("The \"" + name + "\" claim is not a list / JSON array of strings", 0); 498 } 499 } 500 501 return stringArray; 502 } 503 504 505 /** 506 * Gets the specified claims (registered or custom) as a 507 * {@link java.util.List} list of strings. 508 * 509 * @param name The name of the claim. Must not be {@code null}. 510 * 511 * @return The value of the claim, {@code null} if not specified. 512 * 513 * @throws ParseException If the claim value is not of the required 514 * type. 515 */ 516 public List<String> getStringListClaim(final String name) 517 throws ParseException { 518 519 String[] stringArray = getStringArrayClaim(name); 520 521 if (stringArray == null) { 522 return null; 523 } 524 525 return Collections.unmodifiableList(Arrays.asList(stringArray)); 526 } 527 528 529 /** 530 * Gets the specified claim (registered or custom) as 531 * {@link java.lang.Boolean}. 532 * 533 * @param name The name of the claim. Must not be {@code null}. 534 * 535 * @return The value of the claim, {@code null} if not specified. 536 * 537 * @throws ParseException If the claim value is not of the required 538 * type. 539 */ 540 public Boolean getBooleanClaim(final String name) 541 throws ParseException { 542 543 Object value = getClaim(name); 544 545 if (value == null || value instanceof Boolean) { 546 return (Boolean)value; 547 } else { 548 throw new ParseException("The \"" + name + "\" claim is not a Boolean", 0); 549 } 550 } 551 552 553 /** 554 * Gets the specified claim (registered or custom) as 555 * {@link java.lang.Integer}. 556 * 557 * @param name The name of the claim. Must not be {@code null}. 558 * 559 * @return The value of the claim, {@code null} if not specified. 560 * 561 * @throws ParseException If the claim value is not of the required 562 * type. 563 */ 564 public Integer getIntegerClaim(final String name) 565 throws ParseException { 566 567 Object value = getClaim(name); 568 569 if (value == null) { 570 return null; 571 } else if (value instanceof Number) { 572 return ((Number)value).intValue(); 573 } else { 574 throw new ParseException("The \"" + name + "\" claim is not an Integer", 0); 575 } 576 } 577 578 579 /** 580 * Gets the specified claim (registered or custom) as 581 * {@link java.lang.Long}. 582 * 583 * @param name The name of the claim. Must not be {@code null}. 584 * 585 * @return The value of the claim, {@code null} if not specified. 586 * 587 * @throws ParseException If the claim value is not of the required 588 * type. 589 */ 590 public Long getLongClaim(final String name) 591 throws ParseException { 592 593 Object value = getClaim(name); 594 595 if (value == null) { 596 return null; 597 } else if (value instanceof Number) { 598 return ((Number)value).longValue(); 599 } else { 600 throw new ParseException("The \"" + name + "\" claim is not a Number", 0); 601 } 602 } 603 604 605 /** 606 * Gets the specified claim (registered or custom) as 607 * {@link java.util.Date}. The claim may be represented by a Date 608 * object or a number of a seconds since the Unix epoch. 609 * 610 * @param name The name of the claim. Must not be {@code null}. 611 * 612 * @return The value of the claim, {@code null} if not specified. 613 * 614 * @throws ParseException If the claim value is not of the required 615 * type. 616 */ 617 public Date getDateClaim(final String name) 618 throws ParseException { 619 620 Object value = getClaim(name); 621 622 if (value == null) { 623 return null; 624 } else if (value instanceof Date) { 625 return (Date)value; 626 } else if (value instanceof Number) { 627 return new Date(((Number)value).longValue() * 1000l); 628 } else { 629 throw new ParseException("The \"" + name + "\" claim is not a Date", 0); 630 } 631 } 632 633 634 /** 635 * Gets the specified claim (registered or custom) as 636 * {@link java.lang.Float}. 637 * 638 * @param name The name of the claim. Must not be {@code null}. 639 * 640 * @return The value of the claim, {@code null} if not specified. 641 * 642 * @throws ParseException If the claim value is not of the required 643 * type. 644 */ 645 public Float getFloatClaim(final String name) 646 throws ParseException { 647 648 Object value = getClaim(name); 649 650 if (value == null) { 651 return null; 652 } else if (value instanceof Number) { 653 return ((Number)value).floatValue(); 654 } else { 655 throw new ParseException("The \"" + name + "\" claim is not a Float", 0); 656 } 657 } 658 659 660 /** 661 * Gets the specified claim (registered or custom) as 662 * {@link java.lang.Double}. 663 * 664 * @param name The name of the claim. Must not be {@code null}. 665 * 666 * @return The value of the claim, {@code null} if not specified. 667 * 668 * @throws ParseException If the claim value is not of the required 669 * type. 670 */ 671 public Double getDoubleClaim(final String name) 672 throws ParseException { 673 674 Object value = getClaim(name); 675 676 if (value == null) { 677 return null; 678 } else if (value instanceof Number) { 679 return ((Number)value).doubleValue(); 680 } else { 681 throw new ParseException("The \"" + name + "\" claim is not a Double", 0); 682 } 683 } 684 685 686 /** 687 * Gets the claims (registered and custom). 688 * 689 * <p>Note that the registered claims Expiration-Time ({@code exp}), 690 * Not-Before-Time ({@code nbf}) and Issued-At ({@code iat}) will be 691 * returned as {@code java.util.Date} instances. 692 * 693 * @return The claims, as an unmodifiable map, empty map if none. 694 */ 695 public Map<String,Object> getClaims() { 696 697 return Collections.unmodifiableMap(claims); 698 } 699 700 701 /** 702 * Returns the JSON object representation of the claims set. The claims 703 * are serialised according to their insertion order. 704 * 705 * @return The JSON object representation. 706 */ 707 public JSONObject toJSONObject() { 708 709 JSONObject o = new JSONObject(); 710 711 for (Map.Entry<String,Object> claim: claims.entrySet()) { 712 713 if (claim.getValue() instanceof Date) { 714 715 // Transform dates to Unix timestamps 716 Date dateValue = (Date) claim.getValue(); 717 o.put(claim.getKey(), dateValue.getTime() / 1000); 718 719 } else if (AUDIENCE_CLAIM.equals(claim.getKey())) { 720 721 // Serialise single audience list and string 722 List<String> audList = getAudience(); 723 724 if (audList != null && ! audList.isEmpty()) { 725 if (audList.size() == 1) { 726 o.put(AUDIENCE_CLAIM, audList.get(0)); 727 } else { 728 JSONArray audArray = new JSONArray(); 729 audArray.addAll(audList); 730 o.put(AUDIENCE_CLAIM, audArray); 731 } 732 } 733 734 } else if (claim.getValue() != null) { 735 // Do not output claims with null values! 736 o.put(claim.getKey(), claim.getValue()); 737 } 738 } 739 740 return o; 741 } 742 743 744 @Override 745 public String toString() { 746 747 return toJSONObject().toJSONString(); 748 } 749 750 751 /** 752 * Returns a transformation of this JWT claims set. 753 * 754 * @param transformer The JWT claims set transformer. Must not be 755 * {@code null}. 756 * 757 * @return The transformed JWT claims set. 758 */ 759 public <T> T toType(final JWTClaimsSetTransformer<T> transformer) { 760 761 return transformer.transform(this); 762 } 763 764 765 /** 766 * Parses a JSON Web Token (JWT) claims set from the specified JSON 767 * object representation. 768 * 769 * @param json The JSON object to parse. Must not be {@code null}. 770 * 771 * @return The JWT claims set. 772 * 773 * @throws ParseException If the specified JSON object doesn't 774 * represent a valid JWT claims set. 775 */ 776 public static JWTClaimsSet parse(final JSONObject json) 777 throws ParseException { 778 779 JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); 780 781 // Parse registered + custom params 782 for (final String name: json.keySet()) { 783 784 if (name.equals(ISSUER_CLAIM)) { 785 786 builder.issuer(JSONObjectUtils.getString(json, ISSUER_CLAIM)); 787 788 } else if (name.equals(SUBJECT_CLAIM)) { 789 790 builder.subject(JSONObjectUtils.getString(json, SUBJECT_CLAIM)); 791 792 } else if (name.equals(AUDIENCE_CLAIM)) { 793 794 Object audValue = json.get(AUDIENCE_CLAIM); 795 796 if (audValue instanceof String) { 797 List<String> singleAud = new ArrayList<>(); 798 singleAud.add(JSONObjectUtils.getString(json, AUDIENCE_CLAIM)); 799 builder.audience(singleAud); 800 } else if (audValue instanceof List) { 801 builder.audience(JSONObjectUtils.getStringList(json, AUDIENCE_CLAIM)); 802 } 803 804 } else if (name.equals(EXPIRATION_TIME_CLAIM)) { 805 806 builder.expirationTime(new Date(JSONObjectUtils.getLong(json, EXPIRATION_TIME_CLAIM) * 1000)); 807 808 } else if (name.equals(NOT_BEFORE_CLAIM)) { 809 810 builder.notBeforeTime(new Date(JSONObjectUtils.getLong(json, NOT_BEFORE_CLAIM) * 1000)); 811 812 } else if (name.equals(ISSUED_AT_CLAIM)) { 813 814 builder.issueTime(new Date(JSONObjectUtils.getLong(json, ISSUED_AT_CLAIM) * 1000)); 815 816 } else if (name.equals(JWT_ID_CLAIM)) { 817 818 builder.jwtID(JSONObjectUtils.getString(json, JWT_ID_CLAIM)); 819 820 } else { 821 builder.claim(name, json.get(name)); 822 } 823 } 824 825 return builder.build(); 826 } 827 828 829 /** 830 * Parses a JSON Web Token (JWT) claims set from the specified JSON 831 * object string representation. 832 * 833 * @param s The JSON object string to parse. Must not be {@code null}. 834 * 835 * @return The JWT claims set. 836 * 837 * @throws ParseException If the specified JSON object string doesn't 838 * represent a valid JWT claims set. 839 */ 840 public static JWTClaimsSet parse(final String s) 841 throws ParseException { 842 843 return parse(JSONObjectUtils.parseJSONObject(s)); 844 } 845}