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