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}. May involve truncation if 719 * {@link Integer#MAX_VALUE} or {@link Integer#MIN_VALUE} is exceeded. 720 * 721 * @param name The name of the claim. Must not be {@code null}. 722 * 723 * @return The value of the claim, {@code null} if not specified. 724 * 725 * @throws ParseException If the claim value is not of the required 726 * type. 727 */ 728 public Integer getIntegerClaim(final String name) 729 throws ParseException { 730 731 Object value = getClaim(name); 732 733 if (value == null) { 734 return null; 735 } else if (value instanceof Number) { 736 return ((Number)value).intValue(); 737 } else { 738 throw new ParseException("The \"" + name + "\" claim is not an Integer", 0); 739 } 740 } 741 742 743 /** 744 * Gets the specified claim (registered or custom) as 745 * {@link java.lang.Long}. May involve truncation if 746 * {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE} is exceeded. 747 * 748 * @param name The name of the claim. Must not be {@code null}. 749 * 750 * @return The value of the claim, {@code null} if not specified. 751 * 752 * @throws ParseException If the claim value is not of the required 753 * type. 754 */ 755 public Long getLongClaim(final String name) 756 throws ParseException { 757 758 Object value = getClaim(name); 759 760 if (value == null) { 761 return null; 762 } else if (value instanceof Number) { 763 return ((Number)value).longValue(); 764 } else { 765 throw new ParseException("The \"" + name + "\" claim is not a Number", 0); 766 } 767 } 768 769 770 /** 771 * Gets the specified claim (registered or custom) as 772 * {@link java.util.Date}. The claim may be represented by a Date 773 * object or a number of a seconds since the Unix epoch. 774 * 775 * @param name The name of the claim. Must not be {@code null}. 776 * 777 * @return The value of the claim, {@code null} if not specified. 778 * 779 * @throws ParseException If the claim value is not of the required 780 * type. 781 */ 782 public Date getDateClaim(final String name) 783 throws ParseException { 784 785 Object value = getClaim(name); 786 787 if (value == null) { 788 return null; 789 } else if (value instanceof Date) { 790 return (Date)value; 791 } else if (value instanceof Number) { 792 return DateUtils.fromSecondsSinceEpoch(((Number)value).longValue()); 793 } else { 794 throw new ParseException("The \"" + name + "\" claim is not a Date", 0); 795 } 796 } 797 798 799 /** 800 * Gets the specified claim (registered or custom) as 801 * {@link java.lang.Float}. May involve truncation if {@link 802 * Float#MAX_VALUE} or {@link Float#MIN_VALUE} is exceeded, or 803 * rounding if floating-point precision is exceeded. 804 * 805 * @param name The name of the claim. Must not be {@code null}. 806 * 807 * @return The value of the claim, {@code null} if not specified. 808 * 809 * @throws ParseException If the claim value is not of the required 810 * type. 811 */ 812 public Float getFloatClaim(final String name) 813 throws ParseException { 814 815 Object value = getClaim(name); 816 817 if (value == null) { 818 return null; 819 } else if (value instanceof Number) { 820 return ((Number)value).floatValue(); 821 } else { 822 throw new ParseException("The \"" + name + "\" claim is not a Float", 0); 823 } 824 } 825 826 827 /** 828 * Gets the specified claim (registered or custom) as 829 * {@link java.lang.Double}. May involve truncation if {@link 830 * Double#MAX_VALUE} or {@link Double#MIN_VALUE} is exceeded, or 831 * rounding if floating-point precision is exceeded. 832 * 833 * @param name The name of the claim. Must not be {@code null}. 834 * 835 * @return The value of the claim, {@code null} if not specified. 836 * 837 * @throws ParseException If the claim value is not of the required 838 * type. 839 */ 840 public Double getDoubleClaim(final String name) 841 throws ParseException { 842 843 Object value = getClaim(name); 844 845 if (value == null) { 846 return null; 847 } else if (value instanceof Number) { 848 return ((Number)value).doubleValue(); 849 } else { 850 throw new ParseException("The \"" + name + "\" claim is not a Double", 0); 851 } 852 } 853 854 855 /** 856 * Gets the specified claim (registered or custom) as a JSON object. 857 * 858 * @param name The name of the claim. Must not be {@code null}. 859 * 860 * @return The value of the claim, {@code null} if not specified. 861 * 862 * @throws ParseException If the claim value is not of the required 863 * type. 864 */ 865 public Map<String, Object> getJSONObjectClaim(final String name) 866 throws ParseException { 867 868 Object value = getClaim(name); 869 870 if (value == null) { 871 return null; 872 } else if (value instanceof Map) { 873 Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject(); 874 Map<?,?> map = (Map<?,?>)value; 875 for (Map.Entry<?,?> entry: map.entrySet()) { 876 if (entry.getKey() instanceof String) { 877 jsonObject.put((String)entry.getKey(), entry.getValue()); 878 } 879 } 880 return jsonObject; 881 } else { 882 throw new ParseException("The \"" + name + "\" claim is not a JSON object or Map", 0); 883 } 884 } 885 886 887 /** 888 * Gets the claims (registered and custom). 889 * 890 * <p>Note that the registered claims Expiration-Time ({@code exp}), 891 * Not-Before-Time ({@code nbf}) and Issued-At ({@code iat}) will be 892 * returned as {@code java.util.Date} instances. 893 * 894 * @return The claims, as an unmodifiable map, empty map if none. 895 */ 896 public Map<String,Object> getClaims() { 897 898 return Collections.unmodifiableMap(claims); 899 } 900 901 902 /** 903 * Returns a JOSE object payload representation of this claims set. The 904 * claims are serialised according to their insertion order. Claims 905 * with {@code null} values are output according to 906 * {@link Builder#serializeNullClaims(boolean)}. 907 * 908 * @return The payload representation. 909 */ 910 public Payload toPayload() { 911 912 return new Payload(toJSONObject(serializeNullClaims)); 913 } 914 915 916 /** 917 * Returns a JOSE object payload representation of this claims set. The 918 * claims are serialised according to their insertion order. 919 * 920 * @param serializeNullClaims {@code true} to serialise claims with 921 * {@code null} values, {@code false} to 922 * omit them. 923 * 924 * @return The payload representation. 925 */ 926 public Payload toPayload(final boolean serializeNullClaims) { 927 928 return new Payload(toJSONObject(serializeNullClaims)); 929 } 930 931 932 /** 933 * Returns the JSON object representation of this claims set. The 934 * claims are serialised according to their insertion order. Claims 935 * with {@code null} values are output according to 936 * {@link Builder#serializeNullClaims(boolean)}. 937 * 938 * @return The JSON object representation. 939 */ 940 public Map<String, Object> toJSONObject() { 941 942 return toJSONObject(serializeNullClaims); 943 } 944 945 946 /** 947 * Returns the JSON object representation of this claims set. The 948 * claims are serialised according to their insertion order. 949 * 950 * @param serializeNullClaims {@code true} to serialise claims with 951 * {@code null} values, {@code false} to 952 * omit them. 953 * 954 * @return The JSON object representation. 955 */ 956 public Map<String, Object> toJSONObject(final boolean serializeNullClaims) { 957 958 Map<String, Object> o = JSONObjectUtils.newJSONObject(); 959 960 for (Map.Entry<String,Object> claim: claims.entrySet()) { 961 962 if (claim.getValue() instanceof Date) { 963 964 // Transform dates to Unix timestamps 965 Date dateValue = (Date) claim.getValue(); 966 o.put(claim.getKey(), DateUtils.toSecondsSinceEpoch(dateValue)); 967 968 } else if (JWTClaimNames.AUDIENCE.equals(claim.getKey())) { 969 970 // Serialise single audience list and string 971 List<String> audList = getAudience(); 972 973 if (audList != null && ! audList.isEmpty()) { 974 if (audList.size() == 1) { 975 o.put(JWTClaimNames.AUDIENCE, audList.get(0)); 976 } else { 977 List<Object> audArray = JSONArrayUtils.newJSONArray(); 978 audArray.addAll(audList); 979 o.put(JWTClaimNames.AUDIENCE, audArray); 980 } 981 } else if (serializeNullClaims) { 982 o.put(JWTClaimNames.AUDIENCE, null); 983 } 984 985 } else if (claim.getValue() != null) { 986 o.put(claim.getKey(), claim.getValue()); 987 } else if (serializeNullClaims) { 988 o.put(claim.getKey(), null); 989 } 990 } 991 992 return o; 993 } 994 995 996 /** 997 * Returns a JSON object string representation of this claims set. The 998 * claims are serialised according to their insertion order. Claims 999 * with {@code null} values are output according to 1000 * {@link Builder#serializeNullClaims(boolean)}. 1001 * 1002 * @return The JSON object string representation. 1003 */ 1004 @Override 1005 public String toString() { 1006 1007 return JSONObjectUtils.toJSONString(toJSONObject()); 1008 } 1009 1010 1011 /** 1012 * Returns a JSON object string representation of this claims set. The 1013 * claims are serialised according to their insertion order. 1014 * 1015 * @param serializeNullClaims {@code true} to serialise claims with 1016 * {@code null} values, {@code false} to 1017 * omit them. 1018 * 1019 * @return The JSON object string representation. 1020 */ 1021 public String toString(final boolean serializeNullClaims) { 1022 1023 return JSONObjectUtils.toJSONString(toJSONObject(serializeNullClaims)); 1024 } 1025 1026 1027 /** 1028 * Returns a transformation of this JWT claims set. 1029 * 1030 * @param <T> Type of the result. 1031 * @param transformer The JWT claims set transformer. Must not be 1032 * {@code null}. 1033 * 1034 * @return The transformed JWT claims set. 1035 */ 1036 public <T> T toType(final JWTClaimsSetTransformer<T> transformer) { 1037 1038 return transformer.transform(this); 1039 } 1040 1041 1042 /** 1043 * Parses a JSON Web Token (JWT) claims set from the specified JSON 1044 * object representation. 1045 * 1046 * @param json The JSON object to parse. Must not be {@code null}. 1047 * 1048 * @return The JWT claims set. 1049 * 1050 * @throws ParseException If the specified JSON object doesn't 1051 * represent a valid JWT claims set. 1052 */ 1053 public static JWTClaimsSet parse(final Map<String, Object> json) 1054 throws ParseException { 1055 1056 JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); 1057 1058 // Parse registered + custom params 1059 for (final String name: json.keySet()) { 1060 1061 switch (name) { 1062 case JWTClaimNames.ISSUER: 1063 builder.issuer(JSONObjectUtils.getString(json, JWTClaimNames.ISSUER)); 1064 break; 1065 case JWTClaimNames.SUBJECT: 1066 Object subValue = json.get(JWTClaimNames.SUBJECT); 1067 if (subValue instanceof String) { 1068 builder.subject(JSONObjectUtils.getString(json, JWTClaimNames.SUBJECT)); 1069 } else if (subValue instanceof Number) { 1070 // Numbers not allowed per JWT spec, compromise 1071 // to enable interop with non-compliant libs 1072 // https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2 1073 builder.subject(String.valueOf(subValue)); 1074 } else if (subValue == null) { 1075 builder.subject(null); 1076 } else { 1077 throw new ParseException("Illegal " + JWTClaimNames.SUBJECT + " claim", 0); 1078 } 1079 break; 1080 case JWTClaimNames.AUDIENCE: 1081 Object audValue = json.get(JWTClaimNames.AUDIENCE); 1082 if (audValue instanceof String) { 1083 List<String> singleAud = new ArrayList<>(); 1084 singleAud.add(JSONObjectUtils.getString(json, JWTClaimNames.AUDIENCE)); 1085 builder.audience(singleAud); 1086 } else if (audValue instanceof List) { 1087 builder.audience(JSONObjectUtils.getStringList(json, JWTClaimNames.AUDIENCE)); 1088 } else if (audValue == null) { 1089 builder.audience((String) null); 1090 } else { 1091 throw new ParseException("Illegal " + JWTClaimNames.AUDIENCE + " claim", 0); 1092 } 1093 break; 1094 case JWTClaimNames.EXPIRATION_TIME: 1095 builder.expirationTime(JSONObjectUtils.getEpochSecondAsDate(json, JWTClaimNames.EXPIRATION_TIME)); 1096 break; 1097 case JWTClaimNames.NOT_BEFORE: 1098 builder.notBeforeTime(JSONObjectUtils.getEpochSecondAsDate(json, JWTClaimNames.NOT_BEFORE)); 1099 break; 1100 case JWTClaimNames.ISSUED_AT: 1101 builder.issueTime(JSONObjectUtils.getEpochSecondAsDate(json, JWTClaimNames.ISSUED_AT)); 1102 break; 1103 case JWTClaimNames.JWT_ID: 1104 builder.jwtID(JSONObjectUtils.getString(json, JWTClaimNames.JWT_ID)); 1105 break; 1106 default: 1107 builder.claim(name, json.get(name)); 1108 break; 1109 } 1110 } 1111 1112 return builder.build(); 1113 } 1114 1115 1116 /** 1117 * Parses a JSON Web Token (JWT) claims set from the specified JSON 1118 * object string representation. 1119 * 1120 * @param s The JSON object string to parse. Must not be {@code null}. 1121 * 1122 * @return The JWT claims set. 1123 * 1124 * @throws ParseException If the specified JSON object string doesn't 1125 * represent a valid JWT claims set. 1126 */ 1127 public static JWTClaimsSet parse(final String s) 1128 throws ParseException { 1129 1130 return parse(JSONObjectUtils.parse(s)); 1131 } 1132 1133 1134 @Override 1135 public boolean equals(Object o) { 1136 if (this == o) return true; 1137 if (!(o instanceof JWTClaimsSet)) return false; 1138 JWTClaimsSet that = (JWTClaimsSet) o; 1139 return Objects.equals(claims, that.claims); 1140 } 1141 1142 1143 @Override 1144 public int hashCode() { 1145 return Objects.hash(claims); 1146 } 1147}