001package com.nimbusds.jwt; 002 003 004import java.text.ParseException; 005import java.util.ArrayList; 006import java.util.Collections; 007import java.util.Date; 008import java.util.HashMap; 009import java.util.HashSet; 010import java.util.List; 011import java.util.Map; 012import java.util.Set; 013 014import net.minidev.json.JSONArray; 015import net.minidev.json.JSONObject; 016 017import com.nimbusds.jose.util.JSONObjectUtils; 018 019 020/** 021 * JSON Web Token (JWT) claims set. 022 * 023 * <p>Supports all {@link #getRegisteredNames()} registered claims} of the JWT 024 * specification: 025 * 026 * <ul> 027 * <li>iss - Issuer 028 * <li>sub - Subject 029 * <li>aud - Audience 030 * <li>exp - Expiration Time 031 * <li>nbf - Not Before 032 * <li>iat - Issued At 033 * <li>jti - JWT ID 034 * <li>typ - Type 035 * </ul> 036 * 037 * <p>The set may also contain {@link #setCustomClaims custom claims}; these 038 * will be serialised and parsed along the registered ones. 039 * 040 * @author Vladimir Dzhuvinov 041 * @author Justin Richer 042 * @version $version$ (2013-10-07) 043 */ 044public class JWTClaimsSet implements ReadOnlyJWTClaimsSet { 045 046 047 private static final String TYPE_CLAIM = "typ"; 048 private static final String JWT_ID_CLAIM = "jti"; 049 private static final String ISSUED_AT_CLAIM = "iat"; 050 private static final String NOT_BEFORE_CLAIM = "nbf"; 051 private static final String EXPIRATION_TIME_CLAIM = "exp"; 052 private static final String AUDIENCE_CLAIM = "aud"; 053 private static final String SUBJECT_CLAIM = "sub"; 054 private static final String ISSUER_CLAIM = "iss"; 055 056 057 /** 058 * The registered claim names. 059 */ 060 private static final Set<String> REGISTERED_CLAIM_NAMES; 061 062 063 /** 064 * Initialises the registered claim name set. 065 */ 066 static { 067 Set<String> n = new HashSet<String>(); 068 069 n.add(ISSUER_CLAIM); 070 n.add(SUBJECT_CLAIM); 071 n.add(AUDIENCE_CLAIM); 072 n.add(EXPIRATION_TIME_CLAIM); 073 n.add(NOT_BEFORE_CLAIM); 074 n.add(ISSUED_AT_CLAIM); 075 n.add(JWT_ID_CLAIM); 076 n.add(TYPE_CLAIM); 077 078 REGISTERED_CLAIM_NAMES = Collections.unmodifiableSet(n); 079 } 080 081 082 /** 083 * The issuer claim. 084 */ 085 private String iss = null; 086 087 088 /** 089 * The subject claim. 090 */ 091 private String sub = null; 092 093 094 /** 095 * The audience claim. 096 */ 097 private List<String> aud = null; 098 099 100 /** 101 * The expiration time claim. 102 */ 103 private Date exp = null; 104 105 106 /** 107 * The not-before claim. 108 */ 109 private Date nbf = null; 110 111 112 /** 113 * The issued-at claim. 114 */ 115 private Date iat = null; 116 117 118 /** 119 * The JWT ID claim. 120 */ 121 private String jti = null; 122 123 124 /** 125 * The type claim. 126 */ 127 private String typ = null; 128 129 130 /** 131 * Custom claims. 132 */ 133 private Map<String,Object> customClaims = new HashMap<String,Object>(); 134 135 136 /** 137 * Creates a new empty JWT claims set. 138 */ 139 public JWTClaimsSet() { 140 141 // Nothing to do 142 } 143 144 145 /** 146 * Creates a copy of the specified JWT claims set. 147 * 148 * @param old The JWT claims set to copy. Must not be {@code null}. 149 */ 150 public JWTClaimsSet(final ReadOnlyJWTClaimsSet old) { 151 152 super(); 153 setAllClaims(old.getAllClaims()); 154 } 155 156 157 @Override 158 protected Object clone() throws CloneNotSupportedException { 159 160 // TODO Auto-generated method stub 161 return super.clone(); 162 } 163 164 165 /** 166 * Gets the registered JWT claim names. 167 * 168 * @return The registered claim names, as a unmodifiable set. 169 */ 170 public static Set<String> getRegisteredNames() { 171 172 return REGISTERED_CLAIM_NAMES; 173 } 174 175 176 @Override 177 public String getIssuer() { 178 179 return iss; 180 } 181 182 183 /** 184 * Sets the issuer ({@code iss}) claim. 185 * 186 * @param iss The issuer claim, {@code null} if not specified. 187 */ 188 public void setIssuer(final String iss) { 189 190 this.iss = iss; 191 } 192 193 194 @Override 195 public String getSubject() { 196 197 return sub; 198 } 199 200 201 /** 202 * Sets the subject ({@code sub}) claim. 203 * 204 * @param sub The subject claim, {@code null} if not specified. 205 */ 206 public void setSubject(final String sub) { 207 208 this.sub = sub; 209 } 210 211 212 @Override 213 public List<String> getAudience() { 214 215 return aud; 216 } 217 218 219 /** 220 * Sets the audience ({@code aud}) claim. 221 * 222 * @param aud The audience claim, {@code null} if not specified. 223 */ 224 public void setAudience(final List<String> aud) { 225 226 this.aud = aud; 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 Boolean getBooleanClaim(final String name) 413 throws ParseException { 414 415 Object value = getClaim(name); 416 417 if (value == null || value instanceof Boolean) { 418 return (Boolean)value; 419 } else { 420 throw new ParseException("The \"" + name + "\" claim is not a Boolean", 0); 421 } 422 } 423 424 425 @Override 426 public Integer getIntegerClaim(final String name) 427 throws ParseException { 428 429 Object value = getClaim(name); 430 431 if (value == null) { 432 return null; 433 } else if (value instanceof Number) { 434 return ((Number)value).intValue(); 435 } else { 436 throw new ParseException("The \"" + name + "\" claim is not an Integer", 0); 437 } 438 } 439 440 441 @Override 442 public Long getLongClaim(final String name) 443 throws ParseException { 444 445 Object value = getClaim(name); 446 447 if (value == null) { 448 return null; 449 } else if (value instanceof Number) { 450 return ((Number)value).longValue(); 451 } else { 452 throw new ParseException("The \"" + name + "\" claim is not a Number", 0); 453 } 454 } 455 456 457 @Override 458 public Float getFloatClaim(final String name) 459 throws ParseException { 460 461 Object value = getClaim(name); 462 463 if (value == null) { 464 return null; 465 } else if (value instanceof Number) { 466 return ((Number)value).floatValue(); 467 } else { 468 throw new ParseException("The \"" + name + "\" claim is not a Float", 0); 469 } 470 } 471 472 473 @Override 474 public Double getDoubleClaim(final String name) 475 throws ParseException { 476 477 Object value = getClaim(name); 478 479 if (value == null) { 480 return null; 481 } else if (value instanceof Number) { 482 return ((Number)value).doubleValue(); 483 } else { 484 throw new ParseException("The \"" + name + "\" claim is not a Double", 0); 485 } 486 } 487 488 489 /** 490 * Sets the specified claim, whether registered or custom. 491 * 492 * @param name The name of the claim to set. Must not be {@code null}. 493 * @param value The value of the claim to set, {@code null} if not 494 * specified. 495 * 496 * @throws IllegalArgumentException If the claim is registered and its 497 * value is not of the expected type. 498 */ 499 public void setClaim(final String name, final Object value) { 500 501 if (ISSUER_CLAIM.equals(name)) { 502 if (value == null || value instanceof String) { 503 setIssuer((String) value); 504 } else { 505 throw new IllegalArgumentException("Issuer claim must be a String"); 506 } 507 } else if (SUBJECT_CLAIM.equals(name)) { 508 if (value == null || value instanceof String) { 509 setSubject((String) value); 510 } else { 511 throw new IllegalArgumentException("Subject claim must be a String"); 512 } 513 } else if (AUDIENCE_CLAIM.equals(name)) { 514 if (value == null || value instanceof List<?>) { 515 setAudience((List<String>) value); 516 } else { 517 throw new IllegalArgumentException("Audience claim must be a List<String>"); 518 } 519 } else if (EXPIRATION_TIME_CLAIM.equals(name)) { 520 if (value == null || value instanceof Date) { 521 setExpirationTime((Date) value); 522 } else { 523 throw new IllegalArgumentException("Expiration claim must be a Date"); 524 } 525 } else if (NOT_BEFORE_CLAIM.equals(name)) { 526 if (value == null || value instanceof Date) { 527 setNotBeforeTime((Date) value); 528 } else { 529 throw new IllegalArgumentException("Not-before claim must be a Date"); 530 } 531 } else if (ISSUED_AT_CLAIM.equals(name)) { 532 if (value == null || value instanceof Date) { 533 setIssueTime((Date) value); 534 } else { 535 throw new IllegalArgumentException("Issued-at claim must be a Date"); 536 } 537 } else if (JWT_ID_CLAIM.equals(name)) { 538 if (value == null || value instanceof String) { 539 setJWTID((String) value); 540 } else { 541 throw new IllegalArgumentException("JWT-ID claim must be a String"); 542 } 543 } else if (TYPE_CLAIM.equals(name)) { 544 if (value == null || value instanceof String) { 545 setType((String) value); 546 } else { 547 throw new IllegalArgumentException("Type claim must be a String"); 548 } 549 } else { 550 setCustomClaim(name, value); 551 } 552 } 553 554 555 @Override 556 public Map<String, Object> getAllClaims() { 557 558 Map<String, Object> allClaims = new HashMap<String, Object>(); 559 560 allClaims.putAll(customClaims); 561 562 for (String registeredClaim : REGISTERED_CLAIM_NAMES) { 563 564 allClaims.put(registeredClaim, getClaim(registeredClaim)); 565 } 566 567 return Collections.unmodifiableMap(allClaims); 568 } 569 570 571 /** 572 * Sets the claims of this JWT claims set, replacing any existing ones. 573 * 574 * @param newClaims The JWT claims. Must not be {@code null}. 575 */ 576 public void setAllClaims(final Map<String, Object> newClaims) { 577 578 for (String name : newClaims.keySet()) { 579 setClaim(name, newClaims.get(name)); 580 } 581 } 582 583 584 @Override 585 public JSONObject toJSONObject() { 586 587 JSONObject o = new JSONObject(customClaims); 588 589 if (iss != null) { 590 o.put(ISSUER_CLAIM, iss); 591 } 592 593 if (sub != null) { 594 o.put(SUBJECT_CLAIM, sub); 595 } 596 597 if (aud != null) { 598 JSONArray audArray = new JSONArray(); 599 audArray.addAll(aud); 600 o.put(AUDIENCE_CLAIM, audArray); 601 } 602 603 if (exp != null) { 604 o.put(EXPIRATION_TIME_CLAIM, exp.getTime() / 1000); 605 } 606 607 if (nbf != null) { 608 o.put(NOT_BEFORE_CLAIM, nbf.getTime() / 1000); 609 } 610 611 if (iat != null) { 612 o.put(ISSUED_AT_CLAIM, iat.getTime() / 1000); 613 } 614 615 if (jti != null) { 616 o.put(JWT_ID_CLAIM, jti); 617 } 618 619 if (typ != null) { 620 o.put(TYPE_CLAIM, typ); 621 } 622 623 return o; 624 } 625 626 627 /** 628 * Parses a JSON Web Token (JWT) claims set from the specified JSON 629 * object representation. 630 * 631 * @param json The JSON object to parse. Must not be {@code null}. 632 * 633 * @return The JWT claims set. 634 * 635 * @throws ParseException If the specified JSON object doesn't 636 * represent a valid JWT claims set. 637 */ 638 public static JWTClaimsSet parse(final JSONObject json) 639 throws ParseException { 640 641 JWTClaimsSet cs = new JWTClaimsSet(); 642 643 // Parse registered + custom params 644 for (final String name: json.keySet()) { 645 646 if (name.equals(ISSUER_CLAIM)) { 647 648 cs.setIssuer(JSONObjectUtils.getString(json, ISSUER_CLAIM)); 649 650 } else if (name.equals(SUBJECT_CLAIM)) { 651 652 cs.setSubject(JSONObjectUtils.getString(json, SUBJECT_CLAIM)); 653 654 } else if (name.equals(AUDIENCE_CLAIM)) { 655 656 Object audValue = json.get(AUDIENCE_CLAIM); 657 658 if (audValue instanceof String) { 659 List<String> singleAud = new ArrayList<String>(); 660 singleAud.add(JSONObjectUtils.getString(json, AUDIENCE_CLAIM)); 661 cs.setAudience(singleAud); 662 } else if (audValue instanceof List) { 663 cs.setAudience(JSONObjectUtils.getStringList(json, AUDIENCE_CLAIM)); 664 } 665 666 } else if (name.equals(EXPIRATION_TIME_CLAIM)) { 667 668 cs.setExpirationTime(new Date(JSONObjectUtils.getLong(json, EXPIRATION_TIME_CLAIM) * 1000)); 669 670 } else if (name.equals(NOT_BEFORE_CLAIM)) { 671 672 cs.setNotBeforeTime(new Date(JSONObjectUtils.getLong(json, NOT_BEFORE_CLAIM) * 1000)); 673 674 } else if (name.equals(ISSUED_AT_CLAIM)) { 675 676 cs.setIssueTime(new Date(JSONObjectUtils.getLong(json, ISSUED_AT_CLAIM) * 1000)); 677 678 } else if (name.equals(JWT_ID_CLAIM)) { 679 680 cs.setJWTID(JSONObjectUtils.getString(json, JWT_ID_CLAIM)); 681 682 } else if (name.equals(TYPE_CLAIM)) { 683 684 cs.setType(JSONObjectUtils.getString(json, TYPE_CLAIM)); 685 686 } else { 687 cs.setCustomClaim(name, json.get(name)); 688 } 689 } 690 691 return cs; 692 } 693 694 695 /** 696 * Parses a JSON Web Token (JWT) claims set from the specified JSON 697 * object string representation. 698 * 699 * @param s The JSON object string to parse. Must not be {@code null}. 700 * 701 * @return The JWT claims set. 702 * 703 * @throws ParseException If the specified JSON object string doesn't 704 * represent a valid JWT claims set. 705 */ 706 public static JWTClaimsSet parse(final String s) 707 throws ParseException { 708 709 return parse(JSONObjectUtils.parseJSONObject(s)); 710 } 711 712 @Override 713 public String toString() { 714 715 return "JWTClaimsSet [iss=" + iss + ", sub=" + sub + ", aud=" + aud + ", exp=" + exp + ", nbf=" + nbf + ", iat=" + iat + ", jti=" + jti + ", typ=" + typ + ", customClaims=" + customClaims + "]"; 716 } 717}