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