001 package com.nimbusds.jwt; 002 003 004 import java.text.ParseException; 005 import java.util.ArrayList; 006 import java.util.Collections; 007 import java.util.Date; 008 import java.util.HashMap; 009 import java.util.HashSet; 010 import java.util.List; 011 import java.util.Map; 012 import java.util.Set; 013 014 import net.minidev.json.JSONArray; 015 import net.minidev.json.JSONObject; 016 017 import com.nimbusds.jose.util.JSONObjectUtils; 018 019 020 /** 021 * JSON Web Token (JWT) claims set. 022 * 023 * <p>Supports all {@link #getReservedNames reserved 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 carry {@link #setCustomClaims custom claims}; these will 038 * be serialised and parsed along the reserved ones. 039 * 040 * @author Vladimir Dzhuvinov 041 * @author Justin Richer 042 * @version $version$ (2013-04-08) 043 */ 044 public 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 reserved claim names. 059 */ 060 private static final Set<String> RESERVED_CLAIM_NAMES; 061 062 063 /** 064 * Initialises the reserved 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 RESERVED_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 /* (non-Javadoc) 158 * @see java.lang.Object#clone() 159 */ 160 @Override 161 protected Object clone() throws CloneNotSupportedException { 162 163 // TODO Auto-generated method stub 164 return super.clone(); 165 } 166 167 168 /** 169 * Gets the reserved JWT claim names. 170 * 171 * @return The reserved claim names, as an unmodifiable set. 172 */ 173 public static Set<String> getReservedNames() { 174 175 return RESERVED_CLAIM_NAMES; 176 } 177 178 179 @Override 180 public String getIssuer() { 181 182 return iss; 183 } 184 185 186 /** 187 * Sets the issuer ({@code iss}) claim. 188 * 189 * @param iss The issuer claim, {@code null} if not specified. 190 */ 191 public void setIssuer(final String iss) { 192 193 this.iss = iss; 194 } 195 196 197 @Override 198 public String getSubject() { 199 200 return sub; 201 } 202 203 204 /** 205 * Sets the subject ({@code sub}) claim. 206 * 207 * @param sub The subject claim, {@code null} if not specified. 208 */ 209 public void setSubject(final String sub) { 210 211 this.sub = sub; 212 } 213 214 215 @Override 216 public List<String> getAudience() { 217 218 return aud; 219 } 220 221 222 /** 223 * Sets the audience ({@code aud}) clam. 224 * 225 * @param aud The audience claim, {@code null} if not specified. 226 */ 227 public void setAudience(final List<String> aud) { 228 229 this.aud = aud; 230 } 231 232 233 @Override 234 public Date getExpirationTime() { 235 236 return exp; 237 } 238 239 240 /** 241 * Sets the expiration time ({@code exp}) claim. 242 * 243 * @param exp The expiration time, {@code null} if not specified. 244 */ 245 public void setExpirationTime(final Date exp) { 246 247 this.exp = exp; 248 } 249 250 251 @Override 252 public Date getNotBeforeTime() { 253 254 return nbf; 255 } 256 257 258 /** 259 * Sets the not-before ({@code nbf}) claim. 260 * 261 * @param nbf The not-before claim, {@code null} if not specified. 262 */ 263 public void setNotBeforeTime(final Date nbf) { 264 265 this.nbf = nbf; 266 } 267 268 269 @Override 270 public Date getIssueTime() { 271 272 return iat; 273 } 274 275 276 /** 277 * Sets the issued-at ({@code iat}) claim. 278 * 279 * @param iat The issued-at claim, {@code null} if not specified. 280 */ 281 public void setIssueTime(final Date iat) { 282 283 this.iat = iat; 284 } 285 286 287 @Override 288 public String getJWTID() { 289 290 return jti; 291 } 292 293 294 /** 295 * Sets the JWT ID ({@code jti}) claim. 296 * 297 * @param jti The JWT ID claim, {@code null} if not specified. 298 */ 299 public void setJWTID(final String jti) { 300 301 this.jti = jti; 302 } 303 304 305 @Override 306 public String getType() { 307 308 return typ; 309 } 310 311 312 /** 313 * Sets the type ({@code typ}) claim. 314 * 315 * @param typ The type claim, {@code null} if not specified. 316 */ 317 public void setType(final String typ) { 318 319 this.typ = typ; 320 } 321 322 323 @Override 324 public Object getCustomClaim(final String name) { 325 326 return customClaims.get(name); 327 } 328 329 330 /** 331 * Sets a custom (non-reserved) claim. 332 * 333 * @param name The name of the custom claim. Must not be {@code null}. 334 * @param value The value of the custom claim, should map to a valid 335 * JSON entity, {@code null} if not specified. 336 * 337 * @throws IllegalArgumentException If the specified custom claim name 338 * matches a reserved claim name. 339 */ 340 public void setCustomClaim(final String name, final Object value) { 341 342 if (getReservedNames().contains(name)) { 343 344 throw new IllegalArgumentException("The claim name \"" + name + "\" matches a reserved name"); 345 } 346 347 customClaims.put(name, value); 348 } 349 350 351 @Override 352 public Map<String,Object> getCustomClaims() { 353 354 return Collections.unmodifiableMap(customClaims); 355 } 356 357 358 /** 359 * Sets the custom (non-reserved) claims. The values must be 360 * serialisable to a JSON entity, otherwise will be ignored. 361 * 362 * @param customClaims The custom claims, empty map or {@code null} if 363 * none. 364 */ 365 public void setCustomClaims(final Map<String,Object> customClaims) { 366 367 if (customClaims == null) { 368 return; 369 } 370 371 this.customClaims = customClaims; 372 } 373 374 375 @Override 376 public Object getClaim(final String name) { 377 378 if (!getReservedNames().contains(name)) { 379 380 return getCustomClaim(name); 381 382 } else { 383 // it's a reserved name, find out which one 384 if (ISSUER_CLAIM.equals(name)) { 385 return getIssuer(); 386 } else if (SUBJECT_CLAIM.equals(name)) { 387 return getSubject(); 388 } else if (AUDIENCE_CLAIM.equals(name)) { 389 return getAudience(); 390 } else if (EXPIRATION_TIME_CLAIM.equals(name)) { 391 return getExpirationTime(); 392 } else if (NOT_BEFORE_CLAIM.equals(name)) { 393 return getNotBeforeTime(); 394 } else if (ISSUED_AT_CLAIM.equals(name)) { 395 return getIssueTime(); 396 } else if (JWT_ID_CLAIM.equals(name)) { 397 return getJWTID(); 398 } else if (TYPE_CLAIM.equals(name)) { 399 return getType(); 400 } else { 401 // if we fall through down to here, something is wrong 402 throw new IllegalArgumentException("Couldn't find reserved claim: " + name); 403 } 404 } 405 } 406 407 408 /** 409 * Sets the specified claim, whether reserved or custom. 410 * 411 * @param name The name of the claim to set. Must not be {@code null}. 412 * @param value The value of the claim to set. May be {@code null}. 413 * 414 * @throws IllegalArgumentException If the claim is reserved and its 415 * value is not of the expected type. 416 */ 417 public void setClaim(final String name, final Object value) { 418 419 if (!getReservedNames().contains(name)) { 420 setCustomClaim(name, value); 421 } else { 422 // it's a reserved name, find out which one 423 if (ISSUER_CLAIM.equals(name)) { 424 if (value instanceof String) { 425 setIssuer((String)value); 426 } else { 427 throw new IllegalArgumentException("Issuer claim must be a String"); 428 } 429 } else if (SUBJECT_CLAIM.equals(name)) { 430 if (value instanceof String) { 431 setSubject((String)value); 432 } else { 433 throw new IllegalArgumentException("Subject claim must be a String"); 434 } 435 } else if (AUDIENCE_CLAIM.equals(name)) { 436 if (value instanceof List<?>) { 437 setAudience((List<String>)value); 438 } else { 439 throw new IllegalArgumentException("Audience claim must be a List<String>"); 440 } 441 } else if (EXPIRATION_TIME_CLAIM.equals(name)) { 442 if (value instanceof Date) { 443 setExpirationTime((Date)value); 444 } else { 445 throw new IllegalArgumentException("Expiration claim must be a Date"); 446 } 447 } else if (NOT_BEFORE_CLAIM.equals(name)) { 448 if (value instanceof Date) { 449 setNotBeforeTime((Date)value); 450 } else { 451 throw new IllegalArgumentException("Not-before claim must be a Date"); 452 } 453 } else if (ISSUED_AT_CLAIM.equals(name)) { 454 if (value instanceof Date) { 455 setIssueTime((Date)value); 456 } else { 457 throw new IllegalArgumentException("Issued-at claim must be a Date"); 458 } 459 } else if (JWT_ID_CLAIM.equals(name)) { 460 if (value instanceof String) { 461 setJWTID((String)value); 462 } else { 463 throw new IllegalArgumentException("JWT-ID claim must be a String"); 464 } 465 } else if (TYPE_CLAIM.equals(name)) { 466 if (value instanceof String) { 467 setType((String)value); 468 } else { 469 throw new IllegalArgumentException("Type claim must be a String"); 470 } 471 } else { 472 // if we fall through down to here, something is wrong 473 throw new IllegalArgumentException("Couldn't find reserved claim: " + name); 474 } 475 } 476 } 477 478 479 @Override 480 public Map<String, Object> getAllClaims() { 481 482 Map<String, Object> allClaims = new HashMap<String, Object>(); 483 484 allClaims.putAll(customClaims); 485 486 for (String reservedClaim : RESERVED_CLAIM_NAMES) { 487 488 allClaims.put(reservedClaim, getClaim(reservedClaim)); 489 } 490 491 return Collections.unmodifiableMap(allClaims); 492 } 493 494 495 /** 496 * Sets the claims of this JWT claims set, replacing any existing ones. 497 * 498 * @param newClaims The JWT claims. Must not be {@code null}. 499 */ 500 public void setAllClaims(final Map<String, Object> newClaims) { 501 502 for (String name : newClaims.keySet()) { 503 setClaim(name, newClaims.get(name)); 504 } 505 } 506 507 508 @Override 509 public JSONObject toJSONObject() { 510 511 JSONObject o = new JSONObject(customClaims); 512 513 if (iss != null) { 514 o.put(ISSUER_CLAIM, iss); 515 } 516 517 if (sub != null) { 518 o.put(SUBJECT_CLAIM, sub); 519 } 520 521 if (aud != null) { 522 JSONArray audArray = new JSONArray(); 523 audArray.addAll(aud); 524 o.put(AUDIENCE_CLAIM, audArray); 525 } 526 527 if (exp != null) { 528 o.put(EXPIRATION_TIME_CLAIM, exp.getTime() / 1000); 529 } 530 531 if (nbf != null) { 532 o.put(NOT_BEFORE_CLAIM, nbf.getTime() / 1000); 533 } 534 535 if (iat != null) { 536 o.put(ISSUED_AT_CLAIM, iat.getTime() / 1000); 537 } 538 539 if (jti != null) { 540 o.put(JWT_ID_CLAIM, jti); 541 } 542 543 if (typ != null) { 544 o.put(TYPE_CLAIM, typ); 545 } 546 547 return o; 548 } 549 550 551 /** 552 * Parses a JSON Web Token (JWT) claims set from the specified 553 * JSON object representation. 554 * 555 * @param json The JSON object to parse. Must not be {@code null}. 556 * 557 * @return The JWT claims set. 558 * 559 * @throws ParseException If the specified JSON object doesn't 560 * represent a valid JWT claims set. 561 */ 562 public static JWTClaimsSet parse(final JSONObject json) 563 throws ParseException { 564 565 JWTClaimsSet cs = new JWTClaimsSet(); 566 567 // Parse reserved + custom params 568 for (final String name: json.keySet()) { 569 570 if (name.equals(ISSUER_CLAIM)) { 571 572 cs.setIssuer(JSONObjectUtils.getString(json, ISSUER_CLAIM)); 573 } 574 else if (name.equals(SUBJECT_CLAIM)) { 575 576 cs.setSubject(JSONObjectUtils.getString(json, SUBJECT_CLAIM)); 577 } 578 else if (name.equals(AUDIENCE_CLAIM)) { 579 580 Object audValue = json.get(AUDIENCE_CLAIM); 581 582 if (audValue != null && audValue instanceof String) { 583 List<String> singleAud = new ArrayList<String>(); 584 singleAud.add(JSONObjectUtils.getString(json, AUDIENCE_CLAIM)); 585 cs.setAudience(singleAud); 586 } 587 else { 588 cs.setAudience(JSONObjectUtils.getStringList(json, AUDIENCE_CLAIM)); 589 } 590 } 591 else if (name.equals(EXPIRATION_TIME_CLAIM)) { 592 593 cs.setExpirationTime(new Date(JSONObjectUtils.getLong(json, EXPIRATION_TIME_CLAIM) * 1000)); 594 } 595 else if (name.equals(NOT_BEFORE_CLAIM)) { 596 597 cs.setNotBeforeTime(new Date(JSONObjectUtils.getLong(json, NOT_BEFORE_CLAIM) * 1000)); 598 } 599 else if (name.equals(ISSUED_AT_CLAIM)) { 600 601 cs.setIssueTime(new Date(JSONObjectUtils.getLong(json, ISSUED_AT_CLAIM) * 1000)); 602 } 603 else if (name.equals(JWT_ID_CLAIM)) { 604 605 cs.setJWTID(JSONObjectUtils.getString(json, JWT_ID_CLAIM)); 606 } 607 else if (name.equals(TYPE_CLAIM)) { 608 609 cs.setType(JSONObjectUtils.getString(json, TYPE_CLAIM)); 610 } 611 else { 612 cs.setCustomClaim(name, json.get(name)); 613 } 614 } 615 616 return cs; 617 } 618 619 620 /* (non-Javadoc) 621 * @see java.lang.Object#toString() 622 */ 623 @Override 624 public String toString() { 625 626 return "JWTClaimsSet [iss=" + iss + ", sub=" + sub + ", aud=" + aud + ", exp=" + exp + ", nbf=" + nbf + ", iat=" + iat + ", jti=" + jti + ", typ=" + typ + ", customClaims=" + customClaims + "]"; 627 } 628 }