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