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