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