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