xref: /aosp_15_r20/external/tink/java_src/src/main/java/com/google/crypto/tink/jwt/RawJwt.java (revision e7b1675dde1b92d52ec075b0a92829627f2c52a5)
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