1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 //////////////////////////////////////////////////////////////////////////////// 16 17 package com.google.crypto.tink.jwt; 18 19 import com.google.errorprone.annotations.CanIgnoreReturnValue; 20 import com.google.errorprone.annotations.Immutable; 21 import com.google.gson.JsonArray; 22 import com.google.gson.JsonElement; 23 import com.google.gson.JsonNull; 24 import com.google.gson.JsonObject; 25 import com.google.gson.JsonParseException; 26 import com.google.gson.JsonPrimitive; 27 import java.time.Instant; 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.Collections; 31 import java.util.HashSet; 32 import java.util.List; 33 import java.util.Optional; 34 import java.util.Set; 35 36 /** 37 * An unencoded and unsigned <a href="https://tools.ietf.org/html/rfc7519">JSON Web Token</a> (JWT). 38 * 39 * <p>It contains all payload claims and a subset of the headers. It does not contain any headers 40 * that depend on the key, such as "alg" or "kid", because these headers are chosen when the token 41 * is signed and encoded, and should not be chosen by the user. This ensures that the key can be 42 * changed without any changes to the user code. 43 */ 44 @Immutable 45 public final class RawJwt { 46 47 private static final long MAX_TIMESTAMP_VALUE = 253402300799L; // 31 Dec 9999, 23:59:59 GMT 48 49 @SuppressWarnings("Immutable") // We do not mutate the payload. 50 private final JsonObject payload; 51 52 private final Optional<String> typeHeader; 53 RawJwt(Builder builder)54 private RawJwt(Builder builder) { 55 if (!builder.payload.has(JwtNames.CLAIM_EXPIRATION) && !builder.withoutExpiration) { 56 throw new IllegalArgumentException( 57 "neither setExpiration() nor withoutExpiration() was called"); 58 } 59 if (builder.payload.has(JwtNames.CLAIM_EXPIRATION) && builder.withoutExpiration) { 60 throw new IllegalArgumentException( 61 "setExpiration() and withoutExpiration() must not be called together"); 62 } 63 this.typeHeader = builder.typeHeader; 64 this.payload = builder.payload.deepCopy(); 65 } 66 RawJwt(Optional<String> typeHeader, String jsonPayload)67 private RawJwt(Optional<String> typeHeader, String jsonPayload) throws JwtInvalidException { 68 this.typeHeader = typeHeader; 69 this.payload = JsonUtil.parseJson(jsonPayload); 70 validateStringClaim(JwtNames.CLAIM_ISSUER); 71 validateStringClaim(JwtNames.CLAIM_SUBJECT); 72 validateStringClaim(JwtNames.CLAIM_JWT_ID); 73 validateTimestampClaim(JwtNames.CLAIM_EXPIRATION); 74 validateTimestampClaim(JwtNames.CLAIM_NOT_BEFORE); 75 validateTimestampClaim(JwtNames.CLAIM_ISSUED_AT); 76 validateAudienceClaim(); 77 } 78 validateStringClaim(String name)79 private void validateStringClaim(String name) throws JwtInvalidException { 80 if (!this.payload.has(name)) { 81 return; 82 } 83 if (!this.payload.get(name).isJsonPrimitive() 84 || !this.payload.get(name).getAsJsonPrimitive().isString()) { 85 throw new JwtInvalidException("invalid JWT payload: claim " + name + " is not a string."); 86 } 87 } 88 validateTimestampClaim(String name)89 private void validateTimestampClaim(String name) throws JwtInvalidException { 90 if (!this.payload.has(name)) { 91 return; 92 } 93 if (!this.payload.get(name).isJsonPrimitive() 94 || !this.payload.get(name).getAsJsonPrimitive().isNumber()) { 95 throw new JwtInvalidException("invalid JWT payload: claim " + name + " is not a number."); 96 } 97 double timestamp = this.payload.get(name).getAsJsonPrimitive().getAsDouble(); 98 if ((timestamp > MAX_TIMESTAMP_VALUE) || (timestamp < 0)) { 99 throw new JwtInvalidException( 100 "invalid JWT payload: claim " + name + " has an invalid timestamp"); 101 } 102 } 103 validateAudienceClaim()104 private void validateAudienceClaim() throws JwtInvalidException { 105 if (!this.payload.has(JwtNames.CLAIM_AUDIENCE)) { 106 return; 107 } 108 if (this.payload.get(JwtNames.CLAIM_AUDIENCE).isJsonPrimitive() 109 && this.payload.get(JwtNames.CLAIM_AUDIENCE).getAsJsonPrimitive().isString()) { 110 return; 111 } 112 113 // aud is not a string, it must be an JsonArray of strings. 114 // getAudiences makes sure that all entries are strings. 115 List<String> audiences = this.getAudiences(); 116 if (audiences.size() < 1) { 117 throw new JwtInvalidException( 118 "invalid JWT payload: claim " + JwtNames.CLAIM_AUDIENCE + " is present but empty."); 119 } 120 } 121 fromJsonPayload(Optional<String> typeHeader, String jsonPayload)122 static RawJwt fromJsonPayload(Optional<String> typeHeader, String jsonPayload) 123 throws JwtInvalidException { 124 return new RawJwt(typeHeader, jsonPayload); 125 } 126 127 /** 128 * Returns a new RawJwt.Builder. 129 */ newBuilder()130 public static Builder newBuilder() { 131 return new Builder(); 132 } 133 134 /** Builder for RawJwt */ 135 public static final class Builder { 136 private Optional<String> typeHeader; 137 private boolean withoutExpiration; 138 private final JsonObject payload; 139 Builder()140 private Builder() { 141 typeHeader = Optional.empty(); 142 withoutExpiration = false; 143 payload = new JsonObject(); 144 } 145 146 /** 147 * Sets the Type Header Parameter. 148 * 149 * <p>When set, this value should be set to a shortended IANA MediaType, see 150 * https://tools.ietf.org/html/rfc7519#section-5.1 and 151 * https://tools.ietf.org/html/rfc8725#section-3.11 152 */ 153 @CanIgnoreReturnValue setTypeHeader(String value)154 public Builder setTypeHeader(String value) { 155 typeHeader = Optional.of(value); 156 return this; 157 } 158 159 /** 160 * Sets the issuer claim that identifies the principal that issued the JWT. 161 * 162 * <p>https://tools.ietf.org/html/rfc7519#section-4.1.1 163 */ 164 @CanIgnoreReturnValue setIssuer(String value)165 public Builder setIssuer(String value) { 166 if (!JsonUtil.isValidString(value)) { 167 throw new IllegalArgumentException(); 168 } 169 payload.add(JwtNames.CLAIM_ISSUER, new JsonPrimitive(value)); 170 return this; 171 } 172 173 /** 174 * Sets the subject claim identifying the principal that is the subject of the JWT. 175 * 176 * <p>https://tools.ietf.org/html/rfc7519#section-4.1.2 177 */ 178 @CanIgnoreReturnValue setSubject(String value)179 public Builder setSubject(String value) { 180 if (!JsonUtil.isValidString(value)) { 181 throw new IllegalArgumentException(); 182 } 183 payload.add(JwtNames.CLAIM_SUBJECT, new JsonPrimitive(value)); 184 return this; 185 } 186 187 /** 188 * Sets the audience that the JWT is intended for. 189 * 190 * <p>Sets the {@code aud} claim as a string. This method can't be used together with {@code 191 * setAudiences} or {@code addAudience}. 192 * 193 * <p>https://tools.ietf.org/html/rfc7519#section-4.1.3 194 */ 195 @CanIgnoreReturnValue setAudience(String value)196 public Builder setAudience(String value) { 197 if (payload.has(JwtNames.CLAIM_AUDIENCE) 198 && payload.get(JwtNames.CLAIM_AUDIENCE).isJsonArray()) { 199 throw new IllegalArgumentException( 200 "setAudience can't be used together with setAudiences or addAudience"); 201 } 202 if (!JsonUtil.isValidString(value)) { 203 throw new IllegalArgumentException("invalid string"); 204 } 205 payload.add(JwtNames.CLAIM_AUDIENCE, new JsonPrimitive(value)); 206 return this; 207 } 208 209 /** 210 * Sets the audiences that the JWT is intended for. 211 * 212 * <p>Sets the {@code aud} claim as an array of strings. This method can't be used together with 213 * {@code setAudience}. 214 * 215 * <p>https://tools.ietf.org/html/rfc7519#section-4.1.3 216 */ 217 @CanIgnoreReturnValue setAudiences(List<String> values)218 public Builder setAudiences(List<String> values) { 219 if (payload.has(JwtNames.CLAIM_AUDIENCE) 220 && !payload.get(JwtNames.CLAIM_AUDIENCE).isJsonArray()) { 221 throw new IllegalArgumentException("setAudiences can't be used together with setAudience"); 222 } 223 if (values.isEmpty()) { 224 throw new IllegalArgumentException("audiences must not be empty"); 225 } 226 JsonArray audiences = new JsonArray(); 227 for (String value : values) { 228 if (!JsonUtil.isValidString(value)) { 229 throw new IllegalArgumentException("invalid string"); 230 } 231 audiences.add(value); 232 } 233 payload.add(JwtNames.CLAIM_AUDIENCE, audiences); 234 return this; 235 } 236 237 /** 238 * Adds an audience that the JWT is intended for. 239 * 240 * <p>The {@code aud} claim will always be encoded as an array of strings. This method can't be 241 * used together with {@code setAudience}. 242 * 243 * <p>https://tools.ietf.org/html/rfc7519#section-4.1.3 244 */ 245 @CanIgnoreReturnValue addAudience(String value)246 public Builder addAudience(String value) { 247 if (!JsonUtil.isValidString(value)) { 248 throw new IllegalArgumentException("invalid string"); 249 } 250 JsonArray audiences; 251 if (payload.has(JwtNames.CLAIM_AUDIENCE)) { 252 JsonElement aud = payload.get(JwtNames.CLAIM_AUDIENCE); 253 if (!aud.isJsonArray()) { 254 throw new IllegalArgumentException( 255 "addAudience can't be used together with setAudience"); 256 } 257 audiences = aud.getAsJsonArray(); 258 } else { 259 audiences = new JsonArray(); 260 } 261 audiences.add(value); 262 payload.add(JwtNames.CLAIM_AUDIENCE, audiences); 263 return this; 264 } 265 266 /** 267 * Sets the JWT ID claim that provides a unique identifier for the JWT. 268 * 269 * <p>https://tools.ietf.org/html/rfc7519#section-4.1.7 270 */ 271 @CanIgnoreReturnValue setJwtId(String value)272 public Builder setJwtId(String value) { 273 if (!JsonUtil.isValidString(value)) { 274 throw new IllegalArgumentException(); 275 } 276 payload.add(JwtNames.CLAIM_JWT_ID, new JsonPrimitive(value)); 277 return this; 278 } 279 setTimestampClaim(String name, Instant value)280 private void setTimestampClaim(String name, Instant value) { 281 // We round the timestamp to a whole number. We always round down. 282 long timestamp = value.getEpochSecond(); 283 if ((timestamp > MAX_TIMESTAMP_VALUE) || (timestamp < 0)) { 284 throw new IllegalArgumentException( 285 "timestamp of claim " + name + " is out of range"); 286 } 287 payload.add(name, new JsonPrimitive(timestamp)); 288 } 289 290 /** 291 * Sets the {@code exp} claim that identifies the instant on or after which the token MUST NOT 292 * be accepted for processing. 293 * 294 * <p>This API requires {@link java.time.Instant} which is unavailable on Android until API 295 * level 26. To use it on older Android devices, enable API desugaring as shown in 296 * https://developer.android.com/studio/write/java8-support#library-desugaring. 297 * 298 * <p>https://tools.ietf.org/html/rfc7519#section-4.1.4 299 */ 300 @CanIgnoreReturnValue setExpiration(Instant value)301 public Builder setExpiration(Instant value) { 302 setTimestampClaim(JwtNames.CLAIM_EXPIRATION, value); 303 return this; 304 } 305 306 /** 307 * Allow generating tokens without an expiration. 308 * 309 * <p>For most applications of JWT, an expiration date should be set. This function makes sure 310 * that this is not forgotten, by requiring to user to explicitly state that no expiration 311 * should be set. 312 */ 313 @CanIgnoreReturnValue withoutExpiration()314 public Builder withoutExpiration() { 315 this.withoutExpiration = true; 316 return this; 317 } 318 319 /** 320 * Sets the {@code nbf} claim that identifies the instant before which the token MUST NOT be 321 * accepted for processing. 322 * 323 * <p>This API requires {@link java.time.Instant} which is unavailable on Android until API 324 * level 26. To use it on older Android devices, enable API desugaring as shown in 325 * https://developer.android.com/studio/write/java8-support#library-desugaring. 326 * 327 * <p>https://tools.ietf.org/html/rfc7519#section-4.1.5 328 */ 329 @CanIgnoreReturnValue setNotBefore(Instant value)330 public Builder setNotBefore(Instant value) { 331 setTimestampClaim(JwtNames.CLAIM_NOT_BEFORE, value); 332 return this; 333 } 334 335 /** 336 * Sets the {@code iat} claim that identifies the instant at which the JWT was issued. 337 * 338 * <p>This API requires {@link java.time.Instant} which is unavailable on Android until API 339 * level 26. To use it on older Android devices, enable API desugaring as shown in 340 * https://developer.android.com/studio/write/java8-support#library-desugaring. 341 * 342 * <p>https://tools.ietf.org/html/rfc7519#section-4.1.6 343 */ 344 @CanIgnoreReturnValue setIssuedAt(Instant value)345 public Builder setIssuedAt(Instant value) { 346 setTimestampClaim(JwtNames.CLAIM_ISSUED_AT, value); 347 return this; 348 } 349 350 /** Adds a custom claim of type {@code boolean} to the JWT. */ 351 @CanIgnoreReturnValue addBooleanClaim(String name, boolean value)352 public Builder addBooleanClaim(String name, boolean value) { 353 JwtNames.validate(name); 354 payload.add(name, new JsonPrimitive(value)); 355 return this; 356 } 357 358 /** Adds a custom claim of type {@code long} to the JWT. */ 359 @CanIgnoreReturnValue addNumberClaim(String name, long value)360 public Builder addNumberClaim(String name, long value) { 361 JwtNames.validate(name); 362 payload.add(name, new JsonPrimitive(value)); 363 return this; 364 } 365 366 /** Adds a custom claim of type {@code double} to the JWT. */ 367 @CanIgnoreReturnValue addNumberClaim(String name, double value)368 public Builder addNumberClaim(String name, double value) { 369 JwtNames.validate(name); 370 payload.add(name, new JsonPrimitive(value)); 371 return this; 372 } 373 374 /** Adds a custom claim of type {@code String} to the JWT. */ 375 @CanIgnoreReturnValue addStringClaim(String name, String value)376 public Builder addStringClaim(String name, String value) { 377 if (!JsonUtil.isValidString(value)) { 378 throw new IllegalArgumentException(); 379 } 380 JwtNames.validate(name); 381 payload.add(name, new JsonPrimitive(value)); 382 return this; 383 } 384 385 /** Adds a custom claim with value null. */ 386 @CanIgnoreReturnValue addNullClaim(String name)387 public Builder addNullClaim(String name) { 388 JwtNames.validate(name); 389 payload.add(name, JsonNull.INSTANCE); 390 return this; 391 } 392 393 /** Adds a custom claim encoded in a JSON {@code String} to the JWT. */ 394 @CanIgnoreReturnValue addJsonObjectClaim(String name, String encodedJsonObject)395 public Builder addJsonObjectClaim(String name, String encodedJsonObject) 396 throws JwtInvalidException { 397 JwtNames.validate(name); 398 payload.add(name, JsonUtil.parseJson(encodedJsonObject)); 399 return this; 400 } 401 402 /** Adds a custom claim encoded in a JSON {@code String} to the JWT. */ 403 @CanIgnoreReturnValue addJsonArrayClaim(String name, String encodedJsonArray)404 public Builder addJsonArrayClaim(String name, String encodedJsonArray) 405 throws JwtInvalidException { 406 JwtNames.validate(name); 407 payload.add(name, JsonUtil.parseJsonArray(encodedJsonArray)); 408 return this; 409 } 410 build()411 public RawJwt build() { 412 return new RawJwt(this); 413 } 414 } 415 getJsonPayload()416 public String getJsonPayload() { 417 return payload.toString(); 418 } 419 hasBooleanClaim(String name)420 boolean hasBooleanClaim(String name) { 421 JwtNames.validate(name); 422 return (payload.has(name) 423 && payload.get(name).isJsonPrimitive() 424 && payload.get(name).getAsJsonPrimitive().isBoolean()); 425 } 426 getBooleanClaim(String name)427 Boolean getBooleanClaim(String name) throws JwtInvalidException { 428 JwtNames.validate(name); 429 if (!payload.has(name)) { 430 throw new JwtInvalidException("claim " + name + " does not exist"); 431 } 432 if (!payload.get(name).isJsonPrimitive() 433 || !payload.get(name).getAsJsonPrimitive().isBoolean()) { 434 throw new JwtInvalidException("claim " + name + " is not a boolean"); 435 } 436 return payload.get(name).getAsBoolean(); 437 } 438 hasNumberClaim(String name)439 boolean hasNumberClaim(String name) { 440 JwtNames.validate(name); 441 return (payload.has(name) 442 && payload.get(name).isJsonPrimitive() 443 && payload.get(name).getAsJsonPrimitive().isNumber()); 444 } 445 getNumberClaim(String name)446 Double getNumberClaim(String name) throws JwtInvalidException { 447 JwtNames.validate(name); 448 if (!payload.has(name)) { 449 throw new JwtInvalidException("claim " + name + " does not exist"); 450 } 451 if (!payload.get(name).isJsonPrimitive() 452 || !payload.get(name).getAsJsonPrimitive().isNumber()) { 453 throw new JwtInvalidException("claim " + name + " is not a number"); 454 } 455 return payload.get(name).getAsDouble(); 456 } 457 hasStringClaim(String name)458 boolean hasStringClaim(String name) { 459 JwtNames.validate(name); 460 return (payload.has(name) 461 && payload.get(name).isJsonPrimitive() 462 && payload.get(name).getAsJsonPrimitive().isString()); 463 } 464 getStringClaim(String name)465 String getStringClaim(String name) throws JwtInvalidException { 466 JwtNames.validate(name); 467 return getStringClaimInternal(name); 468 } 469 getStringClaimInternal(String name)470 private String getStringClaimInternal(String name) throws JwtInvalidException { 471 if (!payload.has(name)) { 472 throw new JwtInvalidException("claim " + name + " does not exist"); 473 } 474 if (!payload.get(name).isJsonPrimitive() 475 || !payload.get(name).getAsJsonPrimitive().isString()) { 476 throw new JwtInvalidException("claim " + name + " is not a string"); 477 } 478 return payload.get(name).getAsString(); 479 } 480 isNullClaim(String name)481 boolean isNullClaim(String name) { 482 JwtNames.validate(name); 483 try { 484 return JsonNull.INSTANCE.equals(payload.get(name)); 485 } catch (JsonParseException ex) { 486 return false; 487 } 488 } 489 hasJsonObjectClaim(String name)490 boolean hasJsonObjectClaim(String name) { 491 JwtNames.validate(name); 492 return (payload.has(name) && payload.get(name).isJsonObject()); 493 } 494 getJsonObjectClaim(String name)495 String getJsonObjectClaim(String name) throws JwtInvalidException { 496 JwtNames.validate(name); 497 if (!payload.has(name)) { 498 throw new JwtInvalidException("claim " + name + " does not exist"); 499 } 500 if (!payload.get(name).isJsonObject()) { 501 throw new JwtInvalidException("claim " + name + " is not a JSON object"); 502 } 503 return payload.get(name).getAsJsonObject().toString(); 504 } 505 hasJsonArrayClaim(String name)506 boolean hasJsonArrayClaim(String name) { 507 JwtNames.validate(name); 508 return (payload.has(name) && payload.get(name).isJsonArray()); 509 } 510 getJsonArrayClaim(String name)511 String getJsonArrayClaim(String name) throws JwtInvalidException { 512 JwtNames.validate(name); 513 if (!payload.has(name)) { 514 throw new JwtInvalidException("claim " + name + " does not exist"); 515 } 516 if (!payload.get(name).isJsonArray()) { 517 throw new JwtInvalidException("claim " + name + " is not a JSON array"); 518 } 519 return payload.get(name).getAsJsonArray().toString(); 520 } 521 hasTypeHeader()522 boolean hasTypeHeader() { 523 return typeHeader.isPresent(); 524 } 525 getTypeHeader()526 String getTypeHeader() throws JwtInvalidException { 527 if (!typeHeader.isPresent()) { 528 throw new JwtInvalidException("type header is not set"); 529 } 530 return typeHeader.get(); 531 } 532 hasIssuer()533 boolean hasIssuer() { 534 return payload.has(JwtNames.CLAIM_ISSUER); 535 } 536 getIssuer()537 String getIssuer() throws JwtInvalidException { 538 return getStringClaimInternal(JwtNames.CLAIM_ISSUER); 539 } 540 hasSubject()541 boolean hasSubject() { 542 return payload.has(JwtNames.CLAIM_SUBJECT); 543 } 544 getSubject()545 String getSubject() throws JwtInvalidException { 546 return getStringClaimInternal(JwtNames.CLAIM_SUBJECT); 547 } 548 hasJwtId()549 boolean hasJwtId() { 550 return payload.has(JwtNames.CLAIM_JWT_ID); 551 } 552 getJwtId()553 String getJwtId() throws JwtInvalidException { 554 return getStringClaimInternal(JwtNames.CLAIM_JWT_ID); 555 } 556 hasAudiences()557 boolean hasAudiences() { 558 // If an audience claim is present, it is always a JsonArray with length > 0. 559 return payload.has(JwtNames.CLAIM_AUDIENCE); 560 } 561 getAudiences()562 List<String> getAudiences() throws JwtInvalidException { 563 if (!hasAudiences()) { 564 throw new JwtInvalidException("claim aud does not exist"); 565 } 566 JsonElement aud = payload.get(JwtNames.CLAIM_AUDIENCE); 567 if (aud.isJsonPrimitive()) { 568 if (!aud.getAsJsonPrimitive().isString()) { 569 throw new JwtInvalidException( 570 String.format("invalid audience: got %s; want a string", aud)); 571 } 572 return Collections.unmodifiableList(Arrays.asList(aud.getAsString())); 573 } 574 if (!aud.isJsonArray()) { 575 throw new JwtInvalidException("claim aud is not a string or a JSON array"); 576 } 577 578 JsonArray audiences = aud.getAsJsonArray(); 579 List<String> result = new ArrayList<>(audiences.size()); 580 for (int i = 0; i < audiences.size(); i++) { 581 if (!audiences.get(i).isJsonPrimitive() 582 || !audiences.get(i).getAsJsonPrimitive().isString()) { 583 throw new JwtInvalidException( 584 String.format("invalid audience: got %s; want a string", audiences.get(i))); 585 } 586 String audience = audiences.get(i).getAsString(); 587 result.add(audience); 588 } 589 590 return Collections.unmodifiableList(result); 591 } 592 getInstant(String name)593 private Instant getInstant(String name) throws JwtInvalidException { 594 if (!payload.has(name)) { 595 throw new JwtInvalidException("claim " + name + " does not exist"); 596 } 597 if (!payload.get(name).isJsonPrimitive() 598 || !payload.get(name).getAsJsonPrimitive().isNumber()) { 599 throw new JwtInvalidException("claim " + name + " is not a timestamp"); 600 } 601 try { 602 double millis = payload.get(name).getAsJsonPrimitive().getAsDouble() * 1000; 603 return Instant.ofEpochMilli((long) millis); 604 } catch (NumberFormatException ex) { 605 throw new JwtInvalidException("claim " + name + " is not a timestamp: " + ex); 606 } 607 } 608 hasExpiration()609 boolean hasExpiration() { 610 return payload.has(JwtNames.CLAIM_EXPIRATION); 611 } 612 getExpiration()613 Instant getExpiration() throws JwtInvalidException { 614 return getInstant(JwtNames.CLAIM_EXPIRATION); 615 } 616 hasNotBefore()617 boolean hasNotBefore() { 618 return payload.has(JwtNames.CLAIM_NOT_BEFORE); 619 } 620 getNotBefore()621 Instant getNotBefore() throws JwtInvalidException { 622 return getInstant(JwtNames.CLAIM_NOT_BEFORE); 623 } 624 hasIssuedAt()625 boolean hasIssuedAt() { 626 return payload.has(JwtNames.CLAIM_ISSUED_AT); 627 } 628 getIssuedAt()629 Instant getIssuedAt() throws JwtInvalidException { 630 return getInstant(JwtNames.CLAIM_ISSUED_AT); 631 } 632 633 /** Returns all custom claim names. */ customClaimNames()634 Set<String> customClaimNames() { 635 HashSet<String> names = new HashSet<>(); 636 for (String name : this.payload.keySet()) { 637 if (!JwtNames.isRegisteredName(name)) { 638 names.add(name); 639 } 640 } 641 return Collections.unmodifiableSet(names); 642 } 643 644 /** 645 * Returns a brief description of a RawJwt object. The exact details of the representation are 646 * unspecified and subject to change. 647 */ 648 @Override toString()649 public String toString() { 650 JsonObject header = new JsonObject(); 651 if (typeHeader.isPresent()) { 652 header.add("typ", new JsonPrimitive(typeHeader.get())); 653 } 654 return header + "." + payload; 655 } 656 } 657