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 java.io.Serializable;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.text.ParseException;
025import java.util.*;
026
027import com.nimbusds.jose.util.DateUtils;
028import com.nimbusds.jose.util.JSONObjectUtils;
029import net.jcip.annotations.Immutable;
030import net.minidev.json.JSONArray;
031import net.minidev.json.JSONObject;
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; these will be serialised and
051 * parsed along the registered ones.
052 *
053 * <p>Example JWT claims set:
054 *
055 * <pre>
056 * {
057 *   "sub"                        : "joe",
058 *   "exp"                        : 1300819380,
059 *   "http://example.com/is_root" : true
060 * }
061 * </pre>
062 *
063 * <p>Example usage:
064 *
065 * <pre>
066 * JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
067 *     .subject("joe")
068 *     .expirationDate(new Date(1300819380 * 1000l)
069 *     .claim("http://example.com/is_root", true)
070 *     .build();
071 * </pre>
072 *
073 * @author Vladimir Dzhuvinov
074 * @author Justin Richer
075 * @version 2018-08-23
076 */
077@Immutable
078public final class JWTClaimsSet implements Serializable {
079
080
081        private static final long serialVersionUID = 1L;
082
083
084        private static final String ISSUER_CLAIM = "iss";
085        private static final String SUBJECT_CLAIM = "sub";
086        private static final String AUDIENCE_CLAIM = "aud";
087        private static final String EXPIRATION_TIME_CLAIM = "exp";
088        private static final String NOT_BEFORE_CLAIM = "nbf";
089        private static final String ISSUED_AT_CLAIM = "iat";
090        private static final String JWT_ID_CLAIM = "jti";
091
092
093        /**
094         * The registered claim names.
095         */
096        private static final Set<String> REGISTERED_CLAIM_NAMES;
097
098
099        /**
100         * Initialises the registered claim name set.
101         */
102        static {
103                Set<String> n = new HashSet<>();
104
105                n.add(ISSUER_CLAIM);
106                n.add(SUBJECT_CLAIM);
107                n.add(AUDIENCE_CLAIM);
108                n.add(EXPIRATION_TIME_CLAIM);
109                n.add(NOT_BEFORE_CLAIM);
110                n.add(ISSUED_AT_CLAIM);
111                n.add(JWT_ID_CLAIM);
112
113                REGISTERED_CLAIM_NAMES = Collections.unmodifiableSet(n);
114        }
115
116
117        /**
118         * Builder for constructing JSON Web Token (JWT) claims sets.
119         *
120         * <p>Example usage:
121         *
122         * <pre>
123         * JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
124         *     .subject("joe")
125         *     .expirationDate(new Date(1300819380 * 1000l)
126         *     .claim("http://example.com/is_root", true)
127         *     .build();
128         * </pre>
129         */
130        public static class Builder {
131
132
133                /**
134                 * The claims.
135                 */
136                private final Map<String,Object> claims = new LinkedHashMap<>();
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                 * Sets the issuer ({@code iss}) claim.
163                 *
164                 * @param iss The issuer claim, {@code null} if not specified.
165                 *
166                 * @return This builder.
167                 */
168                public Builder issuer(final String iss) {
169
170                        claims.put(ISSUER_CLAIM, iss);
171                        return this;
172                }
173
174
175                /**
176                 * Sets the subject ({@code sub}) claim.
177                 *
178                 * @param sub The subject claim, {@code null} if not specified.
179                 *
180                 * @return This builder.
181                 */
182                public Builder subject(final String sub) {
183
184                        claims.put(SUBJECT_CLAIM, sub);
185                        return this;
186                }
187
188
189                /**
190                 * Sets the audience ({@code aud}) claim.
191                 *
192                 * @param aud The audience claim, {@code null} if not
193                 *            specified.
194                 *
195                 * @return This builder.
196                 */
197                public Builder audience(final List<String> aud) {
198
199                        claims.put(AUDIENCE_CLAIM, aud);
200                        return this;
201                }
202
203
204                /**
205                 * Sets a single-valued audience ({@code aud}) claim.
206                 *
207                 * @param aud The audience claim, {@code null} if not
208                 *            specified.
209                 *
210                 * @return This builder.
211                 */
212                public Builder audience(final String aud) {
213
214                        if (aud == null) {
215                                claims.put(AUDIENCE_CLAIM, null);
216                        } else {
217                                claims.put(AUDIENCE_CLAIM, Collections.singletonList(aud));
218                        }
219                        return this;
220                }
221
222
223                /**
224                 * Sets the expiration time ({@code exp}) claim.
225                 *
226                 * @param exp The expiration time, {@code null} if not
227                 *            specified.
228                 *
229                 * @return This builder.
230                 */
231                public Builder expirationTime(final Date exp) {
232
233                        claims.put(EXPIRATION_TIME_CLAIM, exp);
234                        return this;
235                }
236
237
238                /**
239                 * Sets the not-before ({@code nbf}) claim.
240                 *
241                 * @param nbf The not-before claim, {@code null} if not
242                 *            specified.
243                 *
244                 * @return This builder.
245                 */
246                public Builder notBeforeTime(final Date nbf) {
247
248                        claims.put(NOT_BEFORE_CLAIM, nbf);
249                        return this;
250                }
251
252
253                /**
254                 * Sets the issued-at ({@code iat}) claim.
255                 *
256                 * @param iat The issued-at claim, {@code null} if not
257                 *            specified.
258                 *
259                 * @return This builder.
260                 */
261                public Builder issueTime(final Date iat) {
262
263                        claims.put(ISSUED_AT_CLAIM, iat);
264                        return this;
265                }
266
267
268                /**
269                 * Sets the JWT ID ({@code jti}) claim.
270                 *
271                 * @param jti The JWT ID claim, {@code null} if not specified.
272                 *
273                 * @return This builder.
274                 */
275                public Builder jwtID(final String jti) {
276
277                        claims.put(JWT_ID_CLAIM, jti);
278                        return this;
279                }
280
281
282                /**
283                 * Sets the specified claim (registered or custom).
284                 *
285                 * @param name  The name of the claim to set. Must not be
286                 *              {@code null}.
287                 * @param value The value of the claim to set, {@code null} if
288                 *              not specified. Should map to a JSON entity.
289                 *
290                 * @return This builder.
291                 */
292                public Builder claim(final String name, final Object value) {
293
294                        claims.put(name, value);
295                        return this;
296                }
297
298
299                /**
300                 * Builds a new JWT claims set.
301                 *
302                 * @return The JWT claims set.
303                 */
304                public JWTClaimsSet build() {
305
306                        return new JWTClaimsSet(claims);
307                }
308        }
309
310
311        /**
312         * The claims map.
313         */
314        private final Map<String,Object> claims = new LinkedHashMap<>();
315
316
317        /**
318         * Creates a new JWT claims set.
319         *
320         * @param claims The JWT claims set as a map. Must not be {@code null}.
321         */
322        private JWTClaimsSet(final Map<String,Object> claims) {
323                
324                this.claims.putAll(claims);
325        }
326
327
328        /**
329         * Gets the registered JWT claim names.
330         *
331         * @return The registered claim names, as a unmodifiable set.
332         */
333        public static Set<String> getRegisteredNames() {
334
335                return REGISTERED_CLAIM_NAMES;
336        }
337
338
339        /**
340         * Gets the issuer ({@code iss}) claim.
341         *
342         * @return The issuer claim, {@code null} if not specified.
343         */
344        public String getIssuer() {
345
346                try {
347                        return getStringClaim(ISSUER_CLAIM);
348                } catch (ParseException e) {
349                        return null;
350                }
351        }
352
353
354        /**
355         * Gets the subject ({@code sub}) claim.
356         *
357         * @return The subject claim, {@code null} if not specified.
358         */
359        public String getSubject() {
360
361                try {
362                        return getStringClaim(SUBJECT_CLAIM);
363                } catch (ParseException e) {
364                        return null;
365                }
366        }
367
368
369        /**
370         * Gets the audience ({@code aud}) claim.
371         *
372         * @return The audience claim, empty list if not specified.
373         */
374        public List<String> getAudience() {
375
376                Object audValue = getClaim(AUDIENCE_CLAIM);
377                
378                if (audValue instanceof String) {
379                        // Special case
380                        return Collections.singletonList((String)audValue);
381                }
382                
383                List<String> aud;
384                try {
385                        aud = getStringListClaim(AUDIENCE_CLAIM);
386                } catch (ParseException e) {
387                        return Collections.emptyList();
388                }
389                return aud != null ? Collections.unmodifiableList(aud) : Collections.<String>emptyList();
390        }
391
392
393        /**
394         * Gets the expiration time ({@code exp}) claim.
395         *
396         * @return The expiration time, {@code null} if not specified.
397         */
398        public Date getExpirationTime() {
399
400                try {
401                        return getDateClaim(EXPIRATION_TIME_CLAIM);
402                } catch (ParseException e) {
403                        return null;
404                }
405        }
406
407
408        /**
409         * Gets the not-before ({@code nbf}) claim.
410         *
411         * @return The not-before claim, {@code null} if not specified.
412         */
413        public Date getNotBeforeTime() {
414
415                try {
416                        return getDateClaim(NOT_BEFORE_CLAIM);
417                } catch (ParseException e) {
418                        return null;
419                }
420        }
421
422
423        /**
424         * Gets the issued-at ({@code iat}) claim.
425         *
426         * @return The issued-at claim, {@code null} if not specified.
427         */
428        public Date getIssueTime() {
429
430                try {
431                        return getDateClaim(ISSUED_AT_CLAIM);
432                } catch (ParseException e) {
433                        return null;
434                }
435        }
436
437
438        /**
439         * Gets the JWT ID ({@code jti}) claim.
440         *
441         * @return The JWT ID claim, {@code null} if not specified.
442         */
443        public String getJWTID() {
444
445                try {
446                        return getStringClaim(JWT_ID_CLAIM);
447                } catch (ParseException e) {
448                        return null;
449                }
450        }
451
452
453        /**
454         * Gets the specified claim (registered or custom).
455         *
456         * @param name The name of the claim. Must not be {@code null}.
457         *
458         * @return The value of the claim, {@code null} if not specified.
459         */
460        public Object getClaim(final String name) {
461
462                return claims.get(name);
463        }
464
465
466        /**
467         * Gets the specified claim (registered or custom) as
468         * {@link java.lang.String}.
469         *
470         * @param name The name of the claim. Must not be {@code null}.
471         *
472         * @return The value of the claim, {@code null} if not specified.
473         *
474         * @throws ParseException If the claim value is not of the required
475         *                        type.
476         */
477        public String getStringClaim(final String name)
478                throws ParseException {
479                
480                Object value = getClaim(name);
481                
482                if (value == null || value instanceof String) {
483                        return (String)value;
484                } else {
485                        throw new ParseException("The \"" + name + "\" claim is not a String", 0);
486                }
487        }
488
489
490        /**
491         * Gets the specified claims (registered or custom) as a
492         * {@link java.lang.String} array.
493         *
494         * @param name The name of the claim. Must not be {@code null}.
495         *
496         * @return The value of the claim, {@code null} if not specified.
497         *
498         * @throws ParseException If the claim value is not of the required
499         *                        type.
500         */
501        public String[] getStringArrayClaim(final String name)
502                throws ParseException {
503
504                Object value = getClaim(name);
505
506                if (value == null) {
507                        return null;
508                }
509
510                List<?> list;
511
512                try {
513                        list = (List<?>)getClaim(name);
514
515                } catch (ClassCastException e) {
516                        throw new ParseException("The \"" + name + "\" claim is not a list / JSON array", 0);
517                }
518
519                String[] stringArray = new String[list.size()];
520
521                for (int i=0; i < stringArray.length; i++) {
522
523                        try {
524                                stringArray[i] = (String)list.get(i);
525                        } catch (ClassCastException e) {
526                                throw new ParseException("The \"" + name + "\" claim is not a list / JSON array of strings", 0);
527                        }
528                }
529
530                return stringArray;
531        }
532
533
534        /**
535         * Gets the specified claims (registered or custom) as a
536         * {@link java.util.List} list of strings.
537         *
538         * @param name The name of the claim. Must not be {@code null}.
539         *
540         * @return The value of the claim, {@code null} if not specified.
541         *
542         * @throws ParseException If the claim value is not of the required
543         *                        type.
544         */
545        public List<String> getStringListClaim(final String name)
546                throws ParseException {
547
548                String[] stringArray = getStringArrayClaim(name);
549
550                if (stringArray == null) {
551                        return null;
552                }
553
554                return Collections.unmodifiableList(Arrays.asList(stringArray));
555        }
556        
557        
558        /**
559         * Gets the specified claim (registered or custom) as a
560         * {@link java.net.URI}.
561         *
562         * @param name The name of the claim. Must not be {@code null}.
563         *
564         * @return The value of the claim, {@code null} if not specified.
565         *
566         * @throws ParseException If the claim couldn't be parsed to a URI.
567         */
568        public URI getURIClaim(final String name)
569                throws ParseException {
570                
571                String uriString = getStringClaim(name);
572                
573                if (uriString == null) {
574                        return null;
575                }
576                
577                try {
578                        return new URI(uriString);
579                } catch (URISyntaxException e) {
580                        throw new ParseException("The \"" + name + "\" claim is not a URI: " + e.getMessage(), 0);
581                }
582        }
583
584
585        /**
586         * Gets the specified claim (registered or custom) as
587         * {@link java.lang.Boolean}.
588         *
589         * @param name The name of the claim. Must not be {@code null}.
590         *
591         * @return The value of the claim, {@code null} if not specified.
592         *
593         * @throws ParseException If the claim value is not of the required
594         *                        type.
595         */
596        public Boolean getBooleanClaim(final String name)
597                throws ParseException {
598                
599                Object value = getClaim(name);
600                
601                if (value == null || value instanceof Boolean) {
602                        return (Boolean)value;
603                } else {
604                        throw new ParseException("The \"" + name + "\" claim is not a Boolean", 0);
605                }
606        }
607
608
609        /**
610         * Gets the specified claim (registered or custom) as
611         * {@link java.lang.Integer}.
612         *
613         * @param name The name of the claim. Must not be {@code null}.
614         *
615         * @return The value of the claim, {@code null} if not specified.
616         *
617         * @throws ParseException If the claim value is not of the required
618         *                        type.
619         */
620        public Integer getIntegerClaim(final String name)
621                throws ParseException {
622                
623                Object value = getClaim(name);
624                
625                if (value == null) {
626                        return null;
627                } else if (value instanceof Number) {
628                        return ((Number)value).intValue();
629                } else {
630                        throw new ParseException("The \"" + name + "\" claim is not an Integer", 0);
631                }
632        }
633
634
635        /**
636         * Gets the specified claim (registered or custom) as
637         * {@link java.lang.Long}.
638         *
639         * @param name The name of the claim. Must not be {@code null}.
640         *
641         * @return The value of the claim, {@code null} if not specified.
642         *
643         * @throws ParseException If the claim value is not of the required
644         *                        type.
645         */
646        public Long getLongClaim(final String name)
647                throws ParseException {
648                
649                Object value = getClaim(name);
650                
651                if (value == null) {
652                        return null;
653                } else if (value instanceof Number) {
654                        return ((Number)value).longValue();
655                } else {
656                        throw new ParseException("The \"" + name + "\" claim is not a Number", 0);
657                }
658        }
659
660
661        /**
662         * Gets the specified claim (registered or custom) as
663         * {@link java.util.Date}. The claim may be represented by a Date
664         * object or a number of a seconds since the Unix epoch.
665         *
666         * @param name The name of the claim. Must not be {@code null}.
667         *
668         * @return The value of the claim, {@code null} if not specified.
669         *
670         * @throws ParseException If the claim value is not of the required
671         *                        type.
672         */
673        public Date getDateClaim(final String name)
674                throws ParseException {
675
676                Object value = getClaim(name);
677
678                if (value == null) {
679                        return null;
680                } else if (value instanceof Date) {
681                        return (Date)value;
682                } else if (value instanceof Number) {
683                        return DateUtils.fromSecondsSinceEpoch(((Number)value).longValue());
684                } else {
685                        throw new ParseException("The \"" + name + "\" claim is not a Date", 0);
686                }
687        }
688
689
690        /**
691         * Gets the specified claim (registered or custom) as
692         * {@link java.lang.Float}.
693         *
694         * @param name The name of the claim. Must not be {@code null}.
695         *
696         * @return The value of the claim, {@code null} if not specified.
697         *
698         * @throws ParseException If the claim value is not of the required
699         *                        type.
700         */
701        public Float getFloatClaim(final String name)
702                throws ParseException {
703                
704                Object value = getClaim(name);
705                
706                if (value == null) {
707                        return null;
708                } else if (value instanceof Number) {
709                        return ((Number)value).floatValue();
710                } else {
711                        throw new ParseException("The \"" + name + "\" claim is not a Float", 0);
712                }
713        }
714
715
716        /**
717         * Gets the specified claim (registered or custom) as
718         * {@link java.lang.Double}.
719         *
720         * @param name The name of the claim. Must not be {@code null}.
721         *
722         * @return The value of the claim, {@code null} if not specified.
723         *
724         * @throws ParseException If the claim value is not of the required
725         *                        type.
726         */
727        public Double getDoubleClaim(final String name)
728                throws ParseException {
729                
730                Object value = getClaim(name);
731                
732                if (value == null) {
733                        return null;
734                } else if (value instanceof Number) {
735                        return ((Number)value).doubleValue();
736                } else {
737                        throw new ParseException("The \"" + name + "\" claim is not a Double", 0);
738                }
739        }
740
741
742        /**
743         * Gets the specified claim (registered or custom) as a
744         * {@link net.minidev.json.JSONObject}.
745         *
746         * @param name The name of the claim. Must not be {@code null}.
747         *
748         * @return The value of the claim, {@code null} if not specified.
749         *
750         * @throws ParseException If the claim value is not of the required
751         *                        type.
752         */
753        public JSONObject getJSONObjectClaim(final String name)
754                throws ParseException {
755
756                Object value = getClaim(name);
757
758                if (value == null) {
759                        return null;
760                } else if (value instanceof JSONObject) {
761                        return (JSONObject)value;
762                } else if (value instanceof Map) {
763                        JSONObject jsonObject = new JSONObject();
764                        Map<?,?> map = (Map<?,?>)value;
765                        for (Map.Entry<?,?> entry: map.entrySet()) {
766                                if (entry.getKey() instanceof String) {
767                                        jsonObject.put((String)entry.getKey(), entry.getValue());
768                                }
769                        }
770                        return jsonObject;
771                } else {
772                        throw new ParseException("The \"" + name + "\" claim is not a JSON object or Map", 0);
773                }
774        }
775
776
777        /**
778         * Gets the claims (registered and custom).
779         *
780         * <p>Note that the registered claims Expiration-Time ({@code exp}),
781         * Not-Before-Time ({@code nbf}) and Issued-At ({@code iat}) will be
782         * returned as {@code java.util.Date} instances.
783         *
784         * @return The claims, as an unmodifiable map, empty map if none.
785         */
786        public Map<String,Object> getClaims() {
787
788                return Collections.unmodifiableMap(claims);
789        }
790
791
792        /**
793         * Returns the JSON object representation of the claims set. The claims
794         * are serialised according to their insertion order.
795         *
796         * @return The JSON object representation.
797         */
798        public JSONObject toJSONObject() {
799
800                JSONObject o = new JSONObject();
801
802                for (Map.Entry<String,Object> claim: claims.entrySet()) {
803
804                        if (claim.getValue() instanceof Date) {
805
806                                // Transform dates to Unix timestamps
807                                Date dateValue = (Date) claim.getValue();
808                                o.put(claim.getKey(), DateUtils.toSecondsSinceEpoch(dateValue));
809
810                        } else if (AUDIENCE_CLAIM.equals(claim.getKey())) {
811
812                                // Serialise single audience list and string
813                                List<String> audList = getAudience();
814
815                                if (audList != null && ! audList.isEmpty()) {
816                                        if (audList.size() == 1) {
817                                                o.put(AUDIENCE_CLAIM, audList.get(0));
818                                        } else {
819                                                JSONArray audArray = new JSONArray();
820                                                audArray.addAll(audList);
821                                                o.put(AUDIENCE_CLAIM, audArray);
822                                        }
823                                }
824
825                        } else if (claim.getValue() != null) {
826                                // Do not output claims with null values!
827                                o.put(claim.getKey(), claim.getValue());
828                        }
829                }
830
831                return o;
832        }
833
834
835        @Override
836        public String toString() {
837
838                return toJSONObject().toJSONString();
839        }
840
841
842        /**
843         * Returns a transformation of this JWT claims set.
844         *
845         * @param <T> Type of the result.
846         * @param transformer The JWT claims set transformer. Must not be
847         *                    {@code null}.
848         *
849         * @return The transformed JWT claims set.
850         */
851        public <T> T toType(final JWTClaimsSetTransformer<T> transformer) {
852
853                return transformer.transform(this);
854        }
855
856
857        /**
858         * Parses a JSON Web Token (JWT) claims set from the specified JSON
859         * object representation.
860         *
861         * @param json The JSON object to parse. Must not be {@code null}.
862         *
863         * @return The JWT claims set.
864         *
865         * @throws ParseException If the specified JSON object doesn't 
866         *                        represent a valid JWT claims set.
867         */
868        public static JWTClaimsSet parse(final JSONObject json)
869                throws ParseException {
870
871                JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
872
873                // Parse registered + custom params
874                for (final String name: json.keySet()) {
875
876                        if (name.equals(ISSUER_CLAIM)) {
877
878                                builder.issuer(JSONObjectUtils.getString(json, ISSUER_CLAIM));
879
880                        } else if (name.equals(SUBJECT_CLAIM)) {
881
882                                builder.subject(JSONObjectUtils.getString(json, SUBJECT_CLAIM));
883
884                        } else if (name.equals(AUDIENCE_CLAIM)) {
885
886                                Object audValue = json.get(AUDIENCE_CLAIM);
887
888                                if (audValue instanceof String) {
889                                        List<String> singleAud = new ArrayList<>();
890                                        singleAud.add(JSONObjectUtils.getString(json, AUDIENCE_CLAIM));
891                                        builder.audience(singleAud);
892                                } else if (audValue instanceof List) {
893                                        builder.audience(JSONObjectUtils.getStringList(json, AUDIENCE_CLAIM));
894                                }
895
896                        } else if (name.equals(EXPIRATION_TIME_CLAIM)) {
897
898                                builder.expirationTime(new Date(JSONObjectUtils.getLong(json, EXPIRATION_TIME_CLAIM) * 1000));
899
900                        } else if (name.equals(NOT_BEFORE_CLAIM)) {
901
902                                builder.notBeforeTime(new Date(JSONObjectUtils.getLong(json, NOT_BEFORE_CLAIM) * 1000));
903
904                        } else if (name.equals(ISSUED_AT_CLAIM)) {
905
906                                builder.issueTime(new Date(JSONObjectUtils.getLong(json, ISSUED_AT_CLAIM) * 1000));
907
908                        } else if (name.equals(JWT_ID_CLAIM)) {
909
910                                builder.jwtID(JSONObjectUtils.getString(json, JWT_ID_CLAIM));
911
912                        } else {
913                                builder.claim(name, json.get(name));
914                        }
915                }
916
917                return builder.build();
918        }
919
920
921        /**
922         * Parses a JSON Web Token (JWT) claims set from the specified JSON
923         * object string representation.
924         *
925         * @param s The JSON object string to parse. Must not be {@code null}.
926         *
927         * @return The JWT claims set.
928         *
929         * @throws ParseException If the specified JSON object string doesn't
930         *                        represent a valid JWT claims set.
931         */
932        public static JWTClaimsSet parse(final String s)
933                throws ParseException {
934
935                return parse(JSONObjectUtils.parse(s));
936        }
937}