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