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 com.nimbusds.jose.Payload; 022import com.nimbusds.jose.util.JSONArrayUtils; 023import com.nimbusds.jose.util.JSONObjectUtils; 024import com.nimbusds.jwt.util.DateUtils; 025import net.jcip.annotations.Immutable; 026 027import java.io.Serializable; 028import java.net.URI; 029import java.net.URISyntaxException; 030import java.text.ParseException; 031import java.util.*; 032 033 034/** 035 * JSON Web Token (JWT) claims set. This class is immutable. 036 * 037 * <p>Supports all {@link #getRegisteredNames() registered claims} of the JWT 038 * specification: 039 * 040 * <ul> 041 * <li>iss - Issuer 042 * <li>sub - Subject 043 * <li>aud - Audience 044 * <li>exp - Expiration Time 045 * <li>nbf - Not Before 046 * <li>iat - Issued At 047 * <li>jti - JWT ID 048 * </ul> 049 * 050 * <p>The set may also contain custom claims. 051 * 052 * <p>Claims with {@code null} values will not be serialised with 053 * {@link #toPayload()} / {@link #toJSONObject()} / {@link #toString()} unless 054 * {@link Builder#serializeNullClaims} is enabled. 055 * 056 * <p>Example JWT claims set: 057 * 058 * <pre> 059 * { 060 * "sub" : "joe", 061 * "exp" : 1300819380, 062 * "https://example.com/is_root" : true 063 * } 064 * </pre> 065 * 066 * <p>Example usage: 067 * 068 * <pre> 069 * JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 070 * .subject("joe") 071 * .expirationTime(new Date(1300819380 * 1000l) 072 * .claim("http://example.com/is_root", true) 073 * .build(); 074 * </pre> 075 * 076 * @author Vladimir Dzhuvinov 077 * @author Justin Richer 078 * @author Joey Zhao 079 * @version 2024-12-20 080 */ 081@Immutable 082public final class JWTClaimsSet implements Serializable { 083 084 085 private static final long serialVersionUID = 1L; 086 087 088 /** 089 * The registered claim names. 090 */ 091 private static final Set<String> REGISTERED_CLAIM_NAMES; 092 093 094 /* 095 * Initialises the registered claim name set. 096 */ 097 static { 098 Set<String> n = new HashSet<>(); 099 100 n.add(JWTClaimNames.ISSUER); 101 n.add(JWTClaimNames.SUBJECT); 102 n.add(JWTClaimNames.AUDIENCE); 103 n.add(JWTClaimNames.EXPIRATION_TIME); 104 n.add(JWTClaimNames.NOT_BEFORE); 105 n.add(JWTClaimNames.ISSUED_AT); 106 n.add(JWTClaimNames.JWT_ID); 107 108 REGISTERED_CLAIM_NAMES = Collections.unmodifiableSet(n); 109 } 110 111 112 /** 113 * Builder for constructing JSON Web Token (JWT) claims sets. 114 * 115 * <p>Example usage: 116 * 117 * <pre> 118 * JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 119 * .subject("joe") 120 * .expirationDate(new Date(1300819380 * 1000l) 121 * .claim("http://example.com/is_root", true) 122 * .build(); 123 * </pre> 124 */ 125 public static class Builder { 126 127 128 /** 129 * The claims. 130 */ 131 private final Map<String,Object> claims = new LinkedHashMap<>(); 132 133 134 /** 135 * Controls serialisation of claims with {@code null} values. 136 */ 137 private boolean serializeNullClaims = false; 138 139 140 /** 141 * Creates a new builder. 142 */ 143 public Builder() { 144 145 // Nothing to do 146 } 147 148 149 /** 150 * Creates a new builder with the claims from the specified 151 * set. 152 * 153 * @param jwtClaimsSet The JWT claims set to use. Must not be 154 * {@code null}. 155 */ 156 public Builder(final JWTClaimsSet jwtClaimsSet) { 157 158 claims.putAll(jwtClaimsSet.claims); 159 } 160 161 162 /** 163 * Controls the serialisation of claims with {@code null} 164 * values when {@link #toPayload()} / {@link #toJSONObject()} / 165 * {@link #toString()} is called. Disabled by default. 166 * 167 * @param enable {@code true} to serialise claims with 168 * {@code null} values, {@code false} to omit 169 * them. 170 * 171 * @return This builder. 172 */ 173 public Builder serializeNullClaims(final boolean enable) { 174 175 serializeNullClaims = enable; 176 return this; 177 } 178 179 180 /** 181 * Sets the issuer ({@code iss}) claim. 182 * 183 * @param iss The issuer claim, {@code null} if not specified. 184 * 185 * @return This builder. 186 */ 187 public Builder issuer(final String iss) { 188 189 claims.put(JWTClaimNames.ISSUER, iss); 190 return this; 191 } 192 193 194 /** 195 * Sets the subject ({@code sub}) claim. 196 * 197 * @param sub The subject claim, {@code null} if not specified. 198 * 199 * @return This builder. 200 */ 201 public Builder subject(final String sub) { 202 203 claims.put(JWTClaimNames.SUBJECT, sub); 204 return this; 205 } 206 207 208 /** 209 * Sets the audience ({@code aud}) claim. 210 * 211 * @param aud The audience claim, {@code null} if not 212 * specified. 213 * 214 * @return This builder. 215 */ 216 public Builder audience(final List<String> aud) { 217 218 claims.put(JWTClaimNames.AUDIENCE, aud); 219 return this; 220 } 221 222 223 /** 224 * Sets a single-valued audience ({@code aud}) claim. 225 * 226 * @param aud The audience claim, {@code null} if not 227 * specified. 228 * 229 * @return This builder. 230 */ 231 public Builder audience(final String aud) { 232 233 if (aud == null) { 234 claims.put(JWTClaimNames.AUDIENCE, null); 235 } else { 236 claims.put(JWTClaimNames.AUDIENCE, Collections.singletonList(aud)); 237 } 238 return this; 239 } 240 241 242 /** 243 * Sets the expiration time ({@code exp}) claim. 244 * 245 * @param exp The expiration time, {@code null} if not 246 * specified. 247 * 248 * @return This builder. 249 */ 250 public Builder expirationTime(final Date exp) { 251 252 claims.put(JWTClaimNames.EXPIRATION_TIME, exp); 253 return this; 254 } 255 256 257 /** 258 * Sets the not-before ({@code nbf}) claim. 259 * 260 * @param nbf The not-before claim, {@code null} if not 261 * specified. 262 * 263 * @return This builder. 264 */ 265 public Builder notBeforeTime(final Date nbf) { 266 267 claims.put(JWTClaimNames.NOT_BEFORE, nbf); 268 return this; 269 } 270 271 272 /** 273 * Sets the issued-at ({@code iat}) claim. 274 * 275 * @param iat The issued-at claim, {@code null} if not 276 * specified. 277 * 278 * @return This builder. 279 */ 280 public Builder issueTime(final Date iat) { 281 282 claims.put(JWTClaimNames.ISSUED_AT, iat); 283 return this; 284 } 285 286 287 /** 288 * Sets the JWT ID ({@code jti}) claim. 289 * 290 * @param jti The JWT ID claim, {@code null} if not specified. 291 * 292 * @return This builder. 293 */ 294 public Builder jwtID(final String jti) { 295 296 claims.put(JWTClaimNames.JWT_ID, jti); 297 return this; 298 } 299 300 301 /** 302 * Sets the specified claim (registered or custom). 303 * 304 * @param name The name of the claim to set. Must not be 305 * {@code null}. 306 * @param value The value of the claim to set, {@code null} if 307 * not specified. Should map to a JSON entity. 308 * 309 * @return This builder. 310 */ 311 public Builder claim(final String name, final Object value) { 312 313 claims.put(name, value); 314 return this; 315 } 316 317 318 /** 319 * Gets the claims (registered and custom). 320 * 321 * <p>Note that the registered claims Expiration-Time 322 * ({@code exp}), Not-Before-Time ({@code nbf}) and Issued-At 323 * ({@code iat}) will be returned as {@code java.util.Date} 324 * instances. 325 * 326 * @return The claims, as an unmodifiable map, empty map if 327 * none. 328 */ 329 public Map<String,Object> getClaims() { 330 331 return Collections.unmodifiableMap(claims); 332 } 333 334 335 /** 336 * Builds a new JWT claims set. 337 * 338 * @return The JWT claims set. 339 */ 340 public JWTClaimsSet build() { 341 342 return new JWTClaimsSet(claims, serializeNullClaims); 343 } 344 } 345 346 347 /** 348 * The claims map. 349 */ 350 private final Map<String,Object> claims = new LinkedHashMap<>(); 351 352 353 /** 354 * Controls serialisation of claims with {@code null} values. 355 */ 356 private final boolean serializeNullClaims; 357 358 359 /** 360 * Creates a new JWT claims set. 361 * 362 * @param claims The JWT claims set as a map. Must not be {@code null}. 363 */ 364 private JWTClaimsSet(final Map<String,Object> claims, 365 final boolean serializeNullClaims) { 366 367 this.claims.putAll(claims); 368 this.serializeNullClaims = serializeNullClaims; 369 } 370 371 372 /** 373 * Gets the registered JWT claim names. 374 * 375 * @return The registered claim names, as an unmodifiable set. 376 */ 377 public static Set<String> getRegisteredNames() { 378 379 return REGISTERED_CLAIM_NAMES; 380 } 381 382 383 /** 384 * Gets the issuer ({@code iss}) claim. 385 * 386 * @return The issuer claim, {@code null} if not specified. 387 */ 388 public String getIssuer() { 389 390 try { 391 return getStringClaim(JWTClaimNames.ISSUER); 392 } catch (ParseException e) { 393 return null; 394 } 395 } 396 397 398 /** 399 * Gets the subject ({@code sub}) claim. 400 * 401 * @return The subject claim, {@code null} if not specified. 402 */ 403 public String getSubject() { 404 405 try { 406 return getStringClaim(JWTClaimNames.SUBJECT); 407 } catch (ParseException e) { 408 return null; 409 } 410 } 411 412 413 /** 414 * Gets the audience ({@code aud}) claim. 415 * 416 * @return The audience claim, empty list if not specified. 417 */ 418 public List<String> getAudience() { 419 420 Object audValue = getClaim(JWTClaimNames.AUDIENCE); 421 422 if (audValue instanceof String) { 423 // Special case 424 return Collections.singletonList((String)audValue); 425 } 426 427 List<String> aud; 428 try { 429 aud = getStringListClaim(JWTClaimNames.AUDIENCE); 430 } catch (ParseException e) { 431 return Collections.emptyList(); 432 } 433 return aud != null ? aud : Collections.<String>emptyList(); 434 } 435 436 437 /** 438 * Gets the expiration time ({@code exp}) claim. 439 * 440 * @return The expiration time, {@code null} if not specified. 441 */ 442 public Date getExpirationTime() { 443 444 try { 445 return getDateClaim(JWTClaimNames.EXPIRATION_TIME); 446 } catch (ParseException e) { 447 return null; 448 } 449 } 450 451 452 /** 453 * Gets the not-before ({@code nbf}) claim. 454 * 455 * @return The not-before claim, {@code null} if not specified. 456 */ 457 public Date getNotBeforeTime() { 458 459 try { 460 return getDateClaim(JWTClaimNames.NOT_BEFORE); 461 } catch (ParseException e) { 462 return null; 463 } 464 } 465 466 467 /** 468 * Gets the issued-at ({@code iat}) claim. 469 * 470 * @return The issued-at claim, {@code null} if not specified. 471 */ 472 public Date getIssueTime() { 473 474 try { 475 return getDateClaim(JWTClaimNames.ISSUED_AT); 476 } catch (ParseException e) { 477 return null; 478 } 479 } 480 481 482 /** 483 * Gets the JWT ID ({@code jti}) claim. 484 * 485 * @return The JWT ID claim, {@code null} if not specified. 486 */ 487 public String getJWTID() { 488 489 try { 490 return getStringClaim(JWTClaimNames.JWT_ID); 491 } catch (ParseException e) { 492 return null; 493 } 494 } 495 496 497 /** 498 * Gets the specified claim (registered or custom). 499 * 500 * @param name The name of the claim. Must not be {@code null}. 501 * 502 * @return The value of the claim, {@code null} if not specified. 503 */ 504 public Object getClaim(final String name) { 505 506 return claims.get(name); 507 } 508 509 510 /** 511 * Gets the specified claim (registered or custom) as 512 * {@link java.lang.String}. 513 * 514 * @param name The name of the claim. Must not be {@code null}. 515 * 516 * @return The value of the claim, {@code null} if not specified. 517 * 518 * @throws ParseException If the claim value is not of the required 519 * type. 520 */ 521 public String getStringClaim(final String name) 522 throws ParseException { 523 524 Object value = getClaim(name); 525 526 if (value == null || value instanceof String) { 527 return (String)value; 528 } else { 529 throw new ParseException("The " + name + " claim is not a String", 0); 530 } 531 } 532 533 /** 534 * Gets the specified claim (registered or custom) as 535 * {@link java.lang.String}, primitive or Wrapper types will be converted to 536 * {@link java.lang.String}. 537 * 538 * 539 * @param name The name of the claim. Must not be {@code null}. 540 * 541 * @return The value of the claim, {@code null} if not specified. 542 * 543 * @throws ParseException If the claim value is not and cannot be 544 * automatically converted to {@link java.lang.String}. 545 */ 546 public String getClaimAsString(final String name) 547 throws ParseException { 548 549 Object value = getClaim(name); 550 551 Class<?> clazz; 552 if (value == null || value instanceof String) { 553 return (String) value; 554 } else if ((clazz = value.getClass()).isPrimitive() || isWrapper(clazz)) { 555 return String.valueOf(value); 556 } else { 557 throw new ParseException("The " + name + " claim is not and cannot be converted to a String", 0); 558 } 559 } 560 561 562 /** 563 * Checks if a class is a Java Wrapper class. 564 * 565 * @param clazz The class to check against. 566 * 567 * @return {@code true} if the class is a Wrapper class, otherwise 568 * {@code false}. 569 */ 570 private static boolean isWrapper(Class<?> clazz) { 571 return clazz == Integer.class || clazz == Double.class || clazz == Float.class 572 || clazz == Long.class || clazz == Short.class || clazz == Byte.class 573 || clazz == Character.class || clazz == Boolean.class; 574 } 575 576 577 /** 578 * Gets the specified claims (registered or custom) as a 579 * {@link java.util.List} list of objects. 580 * 581 * @param name The name of the claim. Must not be {@code null}. 582 * 583 * @return The value of the claim, {@code null} if not specified. 584 * 585 * @throws ParseException If the claim value is not of the required 586 * type. 587 */ 588 public List<Object> getListClaim(final String name) 589 throws ParseException { 590 591 Object value = getClaim(name); 592 593 if (value == null) { 594 return null; 595 } 596 597 try { 598 return (List<Object>)getClaim(name); 599 600 } catch (ClassCastException e) { 601 throw new ParseException("The " + name + " claim is not a list / JSON array", 0); 602 } 603 } 604 605 606 /** 607 * Gets the specified claims (registered or custom) as a 608 * {@link java.lang.String} array. 609 * 610 * @param name The name of the claim. Must not be {@code null}. 611 * 612 * @return The value of the claim, {@code null} if not specified. 613 * 614 * @throws ParseException If the claim value is not of the required 615 * type. 616 */ 617 public String[] getStringArrayClaim(final String name) 618 throws ParseException { 619 620 List<?> list = getListClaim(name); 621 622 if (list == null) { 623 return null; 624 } 625 626 String[] stringArray = new String[list.size()]; 627 628 for (int i=0; i < stringArray.length; i++) { 629 630 try { 631 stringArray[i] = (String)list.get(i); 632 } catch (ClassCastException e) { 633 throw new ParseException("The " + name + " claim is not a list / JSON array of strings", 0); 634 } 635 } 636 637 return stringArray; 638 } 639 640 641 /** 642 * Gets the specified claims (registered or custom) as a 643 * {@link java.util.List} list of strings. 644 * 645 * @param name The name of the claim. Must not be {@code null}. 646 * 647 * @return The value of the claim, {@code null} if not specified. 648 * 649 * @throws ParseException If the claim value is not of the required 650 * type. 651 */ 652 public List<String> getStringListClaim(final String name) 653 throws ParseException { 654 655 String[] stringArray = getStringArrayClaim(name); 656 657 if (stringArray == null) { 658 return null; 659 } 660 661 return Collections.unmodifiableList(Arrays.asList(stringArray)); 662 } 663 664 665 /** 666 * Gets the specified claim (registered or custom) as a 667 * {@link java.net.URI}. 668 * 669 * @param name The name of the claim. Must not be {@code null}. 670 * 671 * @return The value of the claim, {@code null} if not specified. 672 * 673 * @throws ParseException If the claim couldn't be parsed to a URI. 674 */ 675 public URI getURIClaim(final String name) 676 throws ParseException { 677 678 String uriString = getStringClaim(name); 679 680 if (uriString == null) { 681 return null; 682 } 683 684 try { 685 return new URI(uriString); 686 } catch (URISyntaxException e) { 687 throw new ParseException("The \"" + name + "\" claim is not a URI: " + e.getMessage(), 0); 688 } 689 } 690 691 692 /** 693 * Gets the specified claim (registered or custom) as 694 * {@link java.lang.Boolean}. 695 * 696 * @param name The name of the claim. Must not be {@code null}. 697 * 698 * @return The value of the claim, {@code null} if not specified. 699 * 700 * @throws ParseException If the claim value is not of the required 701 * type. 702 */ 703 public Boolean getBooleanClaim(final String name) 704 throws ParseException { 705 706 Object value = getClaim(name); 707 708 if (value == null || value instanceof Boolean) { 709 return (Boolean)value; 710 } else { 711 throw new ParseException("The \"" + name + "\" claim is not a Boolean", 0); 712 } 713 } 714 715 716 /** 717 * Gets the specified claim (registered or custom) as 718 * {@link java.lang.Integer}. 719 * 720 * @param name The name of the claim. Must not be {@code null}. 721 * 722 * @return The value of the claim, {@code null} if not specified. 723 * 724 * @throws ParseException If the claim value is not of the required 725 * type. 726 */ 727 public Integer getIntegerClaim(final String name) 728 throws ParseException { 729 730 Object value = getClaim(name); 731 732 if (value == null) { 733 return null; 734 } else if (value instanceof Number) { 735 return ((Number)value).intValue(); 736 } else { 737 throw new ParseException("The \"" + name + "\" claim is not an Integer", 0); 738 } 739 } 740 741 742 /** 743 * Gets the specified claim (registered or custom) as 744 * {@link java.lang.Long}. 745 * 746 * @param name The name of the claim. Must not be {@code null}. 747 * 748 * @return The value of the claim, {@code null} if not specified. 749 * 750 * @throws ParseException If the claim value is not of the required 751 * type. 752 */ 753 public Long getLongClaim(final String name) 754 throws ParseException { 755 756 Object value = getClaim(name); 757 758 if (value == null) { 759 return null; 760 } else if (value instanceof Number) { 761 return ((Number)value).longValue(); 762 } else { 763 throw new ParseException("The \"" + name + "\" claim is not a Number", 0); 764 } 765 } 766 767 768 /** 769 * Gets the specified claim (registered or custom) as 770 * {@link java.util.Date}. The claim may be represented by a Date 771 * object or a number of a seconds since the Unix epoch. 772 * 773 * @param name The name of the claim. Must not be {@code null}. 774 * 775 * @return The value of the claim, {@code null} if not specified. 776 * 777 * @throws ParseException If the claim value is not of the required 778 * type. 779 */ 780 public Date getDateClaim(final String name) 781 throws ParseException { 782 783 Object value = getClaim(name); 784 785 if (value == null) { 786 return null; 787 } else if (value instanceof Date) { 788 return (Date)value; 789 } else if (value instanceof Number) { 790 return DateUtils.fromSecondsSinceEpoch(((Number)value).longValue()); 791 } else { 792 throw new ParseException("The \"" + name + "\" claim is not a Date", 0); 793 } 794 } 795 796 797 /** 798 * Gets the specified claim (registered or custom) as 799 * {@link java.lang.Float}. 800 * 801 * @param name The name of the claim. Must not be {@code null}. 802 * 803 * @return The value of the claim, {@code null} if not specified. 804 * 805 * @throws ParseException If the claim value is not of the required 806 * type. 807 */ 808 public Float getFloatClaim(final String name) 809 throws ParseException { 810 811 Object value = getClaim(name); 812 813 if (value == null) { 814 return null; 815 } else if (value instanceof Number) { 816 return ((Number)value).floatValue(); 817 } else { 818 throw new ParseException("The \"" + name + "\" claim is not a Float", 0); 819 } 820 } 821 822 823 /** 824 * Gets the specified claim (registered or custom) as 825 * {@link java.lang.Double}. 826 * 827 * @param name The name of the claim. Must not be {@code null}. 828 * 829 * @return The value of the claim, {@code null} if not specified. 830 * 831 * @throws ParseException If the claim value is not of the required 832 * type. 833 */ 834 public Double getDoubleClaim(final String name) 835 throws ParseException { 836 837 Object value = getClaim(name); 838 839 if (value == null) { 840 return null; 841 } else if (value instanceof Number) { 842 return ((Number)value).doubleValue(); 843 } else { 844 throw new ParseException("The \"" + name + "\" claim is not a Double", 0); 845 } 846 } 847 848 849 /** 850 * Gets the specified claim (registered or custom) as a JSON object. 851 * 852 * @param name The name of the claim. Must not be {@code null}. 853 * 854 * @return The value of the claim, {@code null} if not specified. 855 * 856 * @throws ParseException If the claim value is not of the required 857 * type. 858 */ 859 public Map<String, Object> getJSONObjectClaim(final String name) 860 throws ParseException { 861 862 Object value = getClaim(name); 863 864 if (value == null) { 865 return null; 866 } else if (value instanceof Map) { 867 Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject(); 868 Map<?,?> map = (Map<?,?>)value; 869 for (Map.Entry<?,?> entry: map.entrySet()) { 870 if (entry.getKey() instanceof String) { 871 jsonObject.put((String)entry.getKey(), entry.getValue()); 872 } 873 } 874 return jsonObject; 875 } else { 876 throw new ParseException("The \"" + name + "\" claim is not a JSON object or Map", 0); 877 } 878 } 879 880 881 /** 882 * Gets the claims (registered and custom). 883 * 884 * <p>Note that the registered claims Expiration-Time ({@code exp}), 885 * Not-Before-Time ({@code nbf}) and Issued-At ({@code iat}) will be 886 * returned as {@code java.util.Date} instances. 887 * 888 * @return The claims, as an unmodifiable map, empty map if none. 889 */ 890 public Map<String,Object> getClaims() { 891 892 return Collections.unmodifiableMap(claims); 893 } 894 895 896 /** 897 * Returns a JOSE object payload representation of this claims set. The 898 * claims are serialised according to their insertion order. Claims 899 * with {@code null} values are output according to 900 * {@link Builder#serializeNullClaims(boolean)}. 901 * 902 * @return The payload representation. 903 */ 904 public Payload toPayload() { 905 906 return new Payload(toJSONObject(serializeNullClaims)); 907 } 908 909 910 /** 911 * Returns a JOSE object payload representation of this claims set. The 912 * claims are serialised according to their insertion order. 913 * 914 * @param serializeNullClaims {@code true} to serialise claims with 915 * {@code null} values, {@code false} to 916 * omit them. 917 * 918 * @return The payload representation. 919 */ 920 public Payload toPayload(final boolean serializeNullClaims) { 921 922 return new Payload(toJSONObject(serializeNullClaims)); 923 } 924 925 926 /** 927 * Returns the JSON object representation of this claims set. The 928 * claims are serialised according to their insertion order. Claims 929 * with {@code null} values are output according to 930 * {@link Builder#serializeNullClaims(boolean)}. 931 * 932 * @return The JSON object representation. 933 */ 934 public Map<String, Object> toJSONObject() { 935 936 return toJSONObject(serializeNullClaims); 937 } 938 939 940 /** 941 * Returns the JSON object representation of this claims set. The 942 * claims are serialised according to their insertion order. 943 * 944 * @param serializeNullClaims {@code true} to serialise claims with 945 * {@code null} values, {@code false} to 946 * omit them. 947 * 948 * @return The JSON object representation. 949 */ 950 public Map<String, Object> toJSONObject(final boolean serializeNullClaims) { 951 952 Map<String, Object> o = JSONObjectUtils.newJSONObject(); 953 954 for (Map.Entry<String,Object> claim: claims.entrySet()) { 955 956 if (claim.getValue() instanceof Date) { 957 958 // Transform dates to Unix timestamps 959 Date dateValue = (Date) claim.getValue(); 960 o.put(claim.getKey(), DateUtils.toSecondsSinceEpoch(dateValue)); 961 962 } else if (JWTClaimNames.AUDIENCE.equals(claim.getKey())) { 963 964 // Serialise single audience list and string 965 List<String> audList = getAudience(); 966 967 if (audList != null && ! audList.isEmpty()) { 968 if (audList.size() == 1) { 969 o.put(JWTClaimNames.AUDIENCE, audList.get(0)); 970 } else { 971 List<Object> audArray = JSONArrayUtils.newJSONArray(); 972 audArray.addAll(audList); 973 o.put(JWTClaimNames.AUDIENCE, audArray); 974 } 975 } else if (serializeNullClaims) { 976 o.put(JWTClaimNames.AUDIENCE, null); 977 } 978 979 } else if (claim.getValue() != null) { 980 o.put(claim.getKey(), claim.getValue()); 981 } else if (serializeNullClaims) { 982 o.put(claim.getKey(), null); 983 } 984 } 985 986 return o; 987 } 988 989 990 /** 991 * Returns a JSON object string representation of this claims set. The 992 * claims are serialised according to their insertion order. Claims 993 * with {@code null} values are output according to 994 * {@link Builder#serializeNullClaims(boolean)}. 995 * 996 * @return The JSON object string representation. 997 */ 998 @Override 999 public String toString() { 1000 1001 return JSONObjectUtils.toJSONString(toJSONObject()); 1002 } 1003 1004 1005 /** 1006 * Returns a JSON object string representation of this claims set. The 1007 * claims are serialised according to their insertion order. 1008 * 1009 * @param serializeNullClaims {@code true} to serialise claims with 1010 * {@code null} values, {@code false} to 1011 * omit them. 1012 * 1013 * @return The JSON object string representation. 1014 */ 1015 public String toString(final boolean serializeNullClaims) { 1016 1017 return JSONObjectUtils.toJSONString(toJSONObject(serializeNullClaims)); 1018 } 1019 1020 1021 /** 1022 * Returns a transformation of this JWT claims set. 1023 * 1024 * @param <T> Type of the result. 1025 * @param transformer The JWT claims set transformer. Must not be 1026 * {@code null}. 1027 * 1028 * @return The transformed JWT claims set. 1029 */ 1030 public <T> T toType(final JWTClaimsSetTransformer<T> transformer) { 1031 1032 return transformer.transform(this); 1033 } 1034 1035 1036 /** 1037 * Parses a JSON Web Token (JWT) claims set from the specified JSON 1038 * object representation. 1039 * 1040 * @param json The JSON object to parse. Must not be {@code null}. 1041 * 1042 * @return The JWT claims set. 1043 * 1044 * @throws ParseException If the specified JSON object doesn't 1045 * represent a valid JWT claims set. 1046 */ 1047 public static JWTClaimsSet parse(final Map<String, Object> json) 1048 throws ParseException { 1049 1050 JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); 1051 1052 // Parse registered + custom params 1053 for (final String name: json.keySet()) { 1054 1055 switch (name) { 1056 case JWTClaimNames.ISSUER: 1057 builder.issuer(JSONObjectUtils.getString(json, JWTClaimNames.ISSUER)); 1058 break; 1059 case JWTClaimNames.SUBJECT: 1060 Object subValue = json.get(JWTClaimNames.SUBJECT); 1061 if (subValue instanceof String) { 1062 builder.subject(JSONObjectUtils.getString(json, JWTClaimNames.SUBJECT)); 1063 } else if (subValue instanceof Number) { 1064 // Numbers not allowed per JWT spec, compromise 1065 // to enable interop with non-compliant libs 1066 // https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2 1067 builder.subject(String.valueOf(subValue)); 1068 } else if (subValue == null) { 1069 builder.subject(null); 1070 } else { 1071 throw new ParseException("Illegal " + JWTClaimNames.SUBJECT + " claim", 0); 1072 } 1073 break; 1074 case JWTClaimNames.AUDIENCE: 1075 Object audValue = json.get(JWTClaimNames.AUDIENCE); 1076 if (audValue instanceof String) { 1077 List<String> singleAud = new ArrayList<>(); 1078 singleAud.add(JSONObjectUtils.getString(json, JWTClaimNames.AUDIENCE)); 1079 builder.audience(singleAud); 1080 } else if (audValue instanceof List) { 1081 builder.audience(JSONObjectUtils.getStringList(json, JWTClaimNames.AUDIENCE)); 1082 } else if (audValue == null) { 1083 builder.audience((String) null); 1084 } else { 1085 throw new ParseException("Illegal " + JWTClaimNames.AUDIENCE + " claim", 0); 1086 } 1087 break; 1088 case JWTClaimNames.EXPIRATION_TIME: 1089 builder.expirationTime(JSONObjectUtils.getEpochSecondAsDate(json, JWTClaimNames.EXPIRATION_TIME)); 1090 break; 1091 case JWTClaimNames.NOT_BEFORE: 1092 builder.notBeforeTime(JSONObjectUtils.getEpochSecondAsDate(json, JWTClaimNames.NOT_BEFORE)); 1093 break; 1094 case JWTClaimNames.ISSUED_AT: 1095 builder.issueTime(JSONObjectUtils.getEpochSecondAsDate(json, JWTClaimNames.ISSUED_AT)); 1096 break; 1097 case JWTClaimNames.JWT_ID: 1098 builder.jwtID(JSONObjectUtils.getString(json, JWTClaimNames.JWT_ID)); 1099 break; 1100 default: 1101 builder.claim(name, json.get(name)); 1102 break; 1103 } 1104 } 1105 1106 return builder.build(); 1107 } 1108 1109 1110 /** 1111 * Parses a JSON Web Token (JWT) claims set from the specified JSON 1112 * object string representation. 1113 * 1114 * @param s The JSON object string to parse. Must not be {@code null}. 1115 * 1116 * @return The JWT claims set. 1117 * 1118 * @throws ParseException If the specified JSON object string doesn't 1119 * represent a valid JWT claims set. 1120 */ 1121 public static JWTClaimsSet parse(final String s) 1122 throws ParseException { 1123 1124 return parse(JSONObjectUtils.parse(s)); 1125 } 1126 1127 1128 @Override 1129 public boolean equals(Object o) { 1130 if (this == o) return true; 1131 if (!(o instanceof JWTClaimsSet)) return false; 1132 JWTClaimsSet that = (JWTClaimsSet) o; 1133 return Objects.equals(claims, that.claims); 1134 } 1135 1136 1137 @Override 1138 public int hashCode() { 1139 return Objects.hash(claims); 1140 } 1141}