001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2016, Connect2id Ltd.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.jwt;
019
020
021import com.nimbusds.jose.Payload;
022import com.nimbusds.jose.util.JSONArrayUtils;
023import com.nimbusds.jose.util.JSONObjectUtils;
024import com.nimbusds.jwt.util.DateUtils;
025import net.jcip.annotations.Immutable;
026
027import java.io.Serializable;
028import java.net.URI;
029import java.net.URISyntaxException;
030import java.text.ParseException;
031import java.util.*;
032
033
034/**
035 * JSON Web Token (JWT) claims set. This class is immutable.
036 *
037 * <p>Supports all {@link #getRegisteredNames() registered claims} of the JWT
038 * specification:
039 *
040 * <ul>
041 *     <li>iss - Issuer
042 *     <li>sub - Subject
043 *     <li>aud - Audience
044 *     <li>exp - Expiration Time
045 *     <li>nbf - Not Before
046 *     <li>iat - Issued At
047 *     <li>jti - JWT ID
048 * </ul>
049 *
050 * <p>The set may also contain custom claims.
051 *
052 * <p>Claims with {@code null} values will not be serialised with
053 * {@link #toPayload()} / {@link #toJSONObject()} / {@link #toString()} unless
054 * {@link Builder#serializeNullClaims} is enabled.
055 *
056 * <p>Example JWT claims set:
057 *
058 * <pre>
059 * {
060 *   "sub"                         : "joe",
061 *   "exp"                         : 1300819380,
062 *   "https://example.com/is_root" : true
063 * }
064 * </pre>
065 *
066 * <p>Example usage:
067 *
068 * <pre>
069 * JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
070 *     .subject("joe")
071 *     .expirationTime(new Date(1300819380 * 1000l)
072 *     .claim("http://example.com/is_root", true)
073 *     .build();
074 * </pre>
075 *
076 * @author Vladimir Dzhuvinov
077 * @author Justin Richer
078 * @author Joey Zhao
079 * @version 2024-12-20
080 */
081@Immutable
082public final class JWTClaimsSet implements Serializable {
083
084
085        private static final long serialVersionUID = 1L;
086
087
088        /**
089         * The registered claim names.
090         */
091        private static final Set<String> REGISTERED_CLAIM_NAMES;
092
093
094        /*
095         * Initialises the registered claim name set.
096         */
097        static {
098                Set<String> n = new HashSet<>();
099
100                n.add(JWTClaimNames.ISSUER);
101                n.add(JWTClaimNames.SUBJECT);
102                n.add(JWTClaimNames.AUDIENCE);
103                n.add(JWTClaimNames.EXPIRATION_TIME);
104                n.add(JWTClaimNames.NOT_BEFORE);
105                n.add(JWTClaimNames.ISSUED_AT);
106                n.add(JWTClaimNames.JWT_ID);
107
108                REGISTERED_CLAIM_NAMES = Collections.unmodifiableSet(n);
109        }
110
111
112        /**
113         * Builder for constructing JSON Web Token (JWT) claims sets.
114         *
115         * <p>Example usage:
116         *
117         * <pre>
118         * JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
119         *     .subject("joe")
120         *     .expirationDate(new Date(1300819380 * 1000l)
121         *     .claim("http://example.com/is_root", true)
122         *     .build();
123         * </pre>
124         */
125        public static class Builder {
126
127
128                /**
129                 * The claims.
130                 */
131                private final Map<String,Object> claims = new LinkedHashMap<>();
132
133
134                /**
135                 * Controls serialisation of claims with {@code null} values.
136                 */
137                private boolean serializeNullClaims = false;
138
139
140                /**
141                 * Creates a new builder.
142                 */
143                public Builder() {
144
145                        // Nothing to do
146                }
147
148
149                /**
150                 * Creates a new builder with the claims from the specified
151                 * set.
152                 *
153                 * @param jwtClaimsSet The JWT claims set to use. Must not be
154                 *                     {@code null}.
155                 */
156                public Builder(final JWTClaimsSet jwtClaimsSet) {
157
158                        claims.putAll(jwtClaimsSet.claims);
159                }
160
161
162                /**
163                 * Controls the serialisation of claims with {@code null}
164                 * values when {@link #toPayload()} / {@link #toJSONObject()} /
165                 * {@link #toString()} is called. Disabled by default.
166                 *
167                 * @param enable {@code true} to serialise claims with
168                 *               {@code null} values, {@code false} to omit
169                 *               them.
170                 *
171                 * @return This builder.
172                 */
173                public Builder serializeNullClaims(final boolean enable) {
174
175                        serializeNullClaims = enable;
176                        return this;
177                }
178
179
180                /**
181                 * Sets the issuer ({@code iss}) claim.
182                 *
183                 * @param iss The issuer claim, {@code null} if not specified.
184                 *
185                 * @return This builder.
186                 */
187                public Builder issuer(final String iss) {
188
189                        claims.put(JWTClaimNames.ISSUER, iss);
190                        return this;
191                }
192
193
194                /**
195                 * Sets the subject ({@code sub}) claim.
196                 *
197                 * @param sub The subject claim, {@code null} if not specified.
198                 *
199                 * @return This builder.
200                 */
201                public Builder subject(final String sub) {
202
203                        claims.put(JWTClaimNames.SUBJECT, sub);
204                        return this;
205                }
206
207
208                /**
209                 * Sets the audience ({@code aud}) claim.
210                 *
211                 * @param aud The audience claim, {@code null} if not
212                 *            specified.
213                 *
214                 * @return This builder.
215                 */
216                public Builder audience(final List<String> aud) {
217
218                        claims.put(JWTClaimNames.AUDIENCE, aud);
219                        return this;
220                }
221
222
223                /**
224                 * Sets a single-valued audience ({@code aud}) claim.
225                 *
226                 * @param aud The audience claim, {@code null} if not
227                 *            specified.
228                 *
229                 * @return This builder.
230                 */
231                public Builder audience(final String aud) {
232
233                        if (aud == null) {
234                                claims.put(JWTClaimNames.AUDIENCE, null);
235                        } else {
236                                claims.put(JWTClaimNames.AUDIENCE, Collections.singletonList(aud));
237                        }
238                        return this;
239                }
240
241
242                /**
243                 * Sets the expiration time ({@code exp}) claim.
244                 *
245                 * @param exp The expiration time, {@code null} if not
246                 *            specified.
247                 *
248                 * @return This builder.
249                 */
250                public Builder expirationTime(final Date exp) {
251
252                        claims.put(JWTClaimNames.EXPIRATION_TIME, exp);
253                        return this;
254                }
255
256
257                /**
258                 * Sets the not-before ({@code nbf}) claim.
259                 *
260                 * @param nbf The not-before claim, {@code null} if not
261                 *            specified.
262                 *
263                 * @return This builder.
264                 */
265                public Builder notBeforeTime(final Date nbf) {
266
267                        claims.put(JWTClaimNames.NOT_BEFORE, nbf);
268                        return this;
269                }
270
271
272                /**
273                 * Sets the issued-at ({@code iat}) claim.
274                 *
275                 * @param iat The issued-at claim, {@code null} if not
276                 *            specified.
277                 *
278                 * @return This builder.
279                 */
280                public Builder issueTime(final Date iat) {
281
282                        claims.put(JWTClaimNames.ISSUED_AT, iat);
283                        return this;
284                }
285
286
287                /**
288                 * Sets the JWT ID ({@code jti}) claim.
289                 *
290                 * @param jti The JWT ID claim, {@code null} if not specified.
291                 *
292                 * @return This builder.
293                 */
294                public Builder jwtID(final String jti) {
295
296                        claims.put(JWTClaimNames.JWT_ID, jti);
297                        return this;
298                }
299
300
301                /**
302                 * Sets the specified claim (registered or custom).
303                 *
304                 * @param name  The name of the claim to set. Must not be
305                 *              {@code null}.
306                 * @param value The value of the claim to set, {@code null} if
307                 *              not specified. Should map to a JSON entity.
308                 *
309                 * @return This builder.
310                 */
311                public Builder claim(final String name, final Object value) {
312
313                        claims.put(name, value);
314                        return this;
315                }
316                
317                
318                /**
319                 * Gets the claims (registered and custom).
320                 *
321                 * <p>Note that the registered claims Expiration-Time
322                 * ({@code exp}), Not-Before-Time ({@code nbf}) and Issued-At
323                 * ({@code iat}) will be returned as {@code java.util.Date}
324                 * instances.
325                 *
326                 * @return The claims, as an unmodifiable map, empty map if
327                 *         none.
328                 */
329                public Map<String,Object> getClaims() {
330                        
331                        return Collections.unmodifiableMap(claims);
332                }
333
334
335                /**
336                 * Builds a new JWT claims set.
337                 *
338                 * @return The JWT claims set.
339                 */
340                public JWTClaimsSet build() {
341
342                        return new JWTClaimsSet(claims, serializeNullClaims);
343                }
344        }
345
346
347        /**
348         * The claims map.
349         */
350        private final Map<String,Object> claims = new LinkedHashMap<>();
351
352
353        /**
354         * Controls serialisation of claims with {@code null} values.
355         */
356        private final boolean serializeNullClaims;
357
358
359        /**
360         * Creates a new JWT claims set.
361         *
362         * @param claims The JWT claims set as a map. Must not be {@code null}.
363         */
364        private JWTClaimsSet(final Map<String,Object> claims,
365                             final boolean serializeNullClaims) {
366                
367                this.claims.putAll(claims);
368                this.serializeNullClaims = serializeNullClaims;
369        }
370
371
372        /**
373         * Gets the registered JWT claim names.
374         *
375         * @return The registered claim names, as an unmodifiable set.
376         */
377        public static Set<String> getRegisteredNames() {
378
379                return REGISTERED_CLAIM_NAMES;
380        }
381
382
383        /**
384         * Gets the issuer ({@code iss}) claim.
385         *
386         * @return The issuer claim, {@code null} if not specified.
387         */
388        public String getIssuer() {
389
390                try {
391                        return getStringClaim(JWTClaimNames.ISSUER);
392                } catch (ParseException e) {
393                        return null;
394                }
395        }
396
397
398        /**
399         * Gets the subject ({@code sub}) claim.
400         *
401         * @return The subject claim, {@code null} if not specified.
402         */
403        public String getSubject() {
404
405                try {
406                        return getStringClaim(JWTClaimNames.SUBJECT);
407                } catch (ParseException e) {
408                        return null;
409                }
410        }
411
412
413        /**
414         * Gets the audience ({@code aud}) claim.
415         *
416         * @return The audience claim, empty list if not specified.
417         */
418        public List<String> getAudience() {
419
420                Object audValue = getClaim(JWTClaimNames.AUDIENCE);
421                
422                if (audValue instanceof String) {
423                        // Special case
424                        return Collections.singletonList((String)audValue);
425                }
426                
427                List<String> aud;
428                try {
429                        aud = getStringListClaim(JWTClaimNames.AUDIENCE);
430                } catch (ParseException e) {
431                        return Collections.emptyList();
432                }
433                return aud != null ? aud : Collections.<String>emptyList();
434        }
435
436
437        /**
438         * Gets the expiration time ({@code exp}) claim.
439         *
440         * @return The expiration time, {@code null} if not specified.
441         */
442        public Date getExpirationTime() {
443
444                try {
445                        return getDateClaim(JWTClaimNames.EXPIRATION_TIME);
446                } catch (ParseException e) {
447                        return null;
448                }
449        }
450
451
452        /**
453         * Gets the not-before ({@code nbf}) claim.
454         *
455         * @return The not-before claim, {@code null} if not specified.
456         */
457        public Date getNotBeforeTime() {
458
459                try {
460                        return getDateClaim(JWTClaimNames.NOT_BEFORE);
461                } catch (ParseException e) {
462                        return null;
463                }
464        }
465
466
467        /**
468         * Gets the issued-at ({@code iat}) claim.
469         *
470         * @return The issued-at claim, {@code null} if not specified.
471         */
472        public Date getIssueTime() {
473
474                try {
475                        return getDateClaim(JWTClaimNames.ISSUED_AT);
476                } catch (ParseException e) {
477                        return null;
478                }
479        }
480
481
482        /**
483         * Gets the JWT ID ({@code jti}) claim.
484         *
485         * @return The JWT ID claim, {@code null} if not specified.
486         */
487        public String getJWTID() {
488
489                try {
490                        return getStringClaim(JWTClaimNames.JWT_ID);
491                } catch (ParseException e) {
492                        return null;
493                }
494        }
495
496
497        /**
498         * Gets the specified claim (registered or custom).
499         *
500         * @param name The name of the claim. Must not be {@code null}.
501         *
502         * @return The value of the claim, {@code null} if not specified.
503         */
504        public Object getClaim(final String name) {
505
506                return claims.get(name);
507        }
508
509
510        /**
511         * Gets the specified claim (registered or custom) as
512         * {@link java.lang.String}.
513         *
514         * @param name The name of the claim. Must not be {@code null}.
515         *
516         * @return The value of the claim, {@code null} if not specified.
517         *
518         * @throws ParseException If the claim value is not of the required
519         *                        type.
520         */
521        public String getStringClaim(final String name)
522                throws ParseException {
523                
524                Object value = getClaim(name);
525                
526                if (value == null || value instanceof String) {
527                        return (String)value;
528                } else {
529                        throw new ParseException("The " + name + " claim is not a String", 0);
530                }
531        }
532
533        /**
534     * Gets the specified claim (registered or custom) as
535     * {@link java.lang.String}, primitive or Wrapper types will be converted to
536     * {@link java.lang.String}.
537     *
538     *
539     * @param name The name of the claim. Must not be {@code null}.
540     *
541     * @return The value of the claim, {@code null} if not specified.
542     *
543     * @throws ParseException If the claim value is not and cannot be
544     *                        automatically converted to {@link java.lang.String}.
545     */
546    public String getClaimAsString(final String name)
547            throws ParseException {
548
549        Object value = getClaim(name);
550
551        Class<?> clazz;
552        if (value == null || value instanceof String) {
553            return (String) value;
554        } else if ((clazz = value.getClass()).isPrimitive() || isWrapper(clazz)) {
555            return String.valueOf(value);
556        } else {
557            throw new ParseException("The " + name + " claim is not and cannot be converted to a String", 0);
558        }
559    }
560
561
562    /**
563     * Checks if a class is a Java Wrapper class.
564     *
565     * @param clazz The class to check against.
566     *
567     * @return {@code true} if the class is a Wrapper class, otherwise
568     * {@code false}.
569     */
570    private static boolean isWrapper(Class<?> clazz) {
571        return clazz == Integer.class || clazz == Double.class || clazz == Float.class
572                || clazz == Long.class || clazz == Short.class || clazz == Byte.class
573                || clazz == Character.class || clazz == Boolean.class;
574    }
575
576
577        /**
578         * Gets the specified claims (registered or custom) as a
579         * {@link java.util.List} list of objects.
580         *
581         * @param name The name of the claim. Must not be {@code null}.
582         *
583         * @return The value of the claim, {@code null} if not specified.
584         *
585         * @throws ParseException If the claim value is not of the required
586         *                        type.
587         */
588        public List<Object> getListClaim(final String name)
589                throws ParseException {
590
591                Object value = getClaim(name);
592
593                if (value == null) {
594                        return null;
595                }
596
597                try {
598                        return (List<Object>)getClaim(name);
599
600                } catch (ClassCastException e) {
601                        throw new ParseException("The " + name + " claim is not a list / JSON array", 0);
602                }
603        }
604
605
606        /**
607         * Gets the specified claims (registered or custom) as a
608         * {@link java.lang.String} array.
609         *
610         * @param name The name of the claim. Must not be {@code null}.
611         *
612         * @return The value of the claim, {@code null} if not specified.
613         *
614         * @throws ParseException If the claim value is not of the required
615         *                        type.
616         */
617        public String[] getStringArrayClaim(final String name)
618                throws ParseException {
619
620                List<?> list = getListClaim(name);
621
622                if (list == null) {
623                        return null;
624                }
625
626                String[] stringArray = new String[list.size()];
627
628                for (int i=0; i < stringArray.length; i++) {
629
630                        try {
631                                stringArray[i] = (String)list.get(i);
632                        } catch (ClassCastException e) {
633                                throw new ParseException("The " + name + " claim is not a list / JSON array of strings", 0);
634                        }
635                }
636
637                return stringArray;
638        }
639
640
641        /**
642         * Gets the specified claims (registered or custom) as a
643         * {@link java.util.List} list of strings.
644         *
645         * @param name The name of the claim. Must not be {@code null}.
646         *
647         * @return The value of the claim, {@code null} if not specified.
648         *
649         * @throws ParseException If the claim value is not of the required
650         *                        type.
651         */
652        public List<String> getStringListClaim(final String name)
653                throws ParseException {
654
655                String[] stringArray = getStringArrayClaim(name);
656
657                if (stringArray == null) {
658                        return null;
659                }
660
661                return Collections.unmodifiableList(Arrays.asList(stringArray));
662        }
663        
664        
665        /**
666         * Gets the specified claim (registered or custom) as a
667         * {@link java.net.URI}.
668         *
669         * @param name The name of the claim. Must not be {@code null}.
670         *
671         * @return The value of the claim, {@code null} if not specified.
672         *
673         * @throws ParseException If the claim couldn't be parsed to a URI.
674         */
675        public URI getURIClaim(final String name)
676                throws ParseException {
677                
678                String uriString = getStringClaim(name);
679                
680                if (uriString == null) {
681                        return null;
682                }
683                
684                try {
685                        return new URI(uriString);
686                } catch (URISyntaxException e) {
687                        throw new ParseException("The \"" + name + "\" claim is not a URI: " + e.getMessage(), 0);
688                }
689        }
690
691
692        /**
693         * Gets the specified claim (registered or custom) as
694         * {@link java.lang.Boolean}.
695         *
696         * @param name The name of the claim. Must not be {@code null}.
697         *
698         * @return The value of the claim, {@code null} if not specified.
699         *
700         * @throws ParseException If the claim value is not of the required
701         *                        type.
702         */
703        public Boolean getBooleanClaim(final String name)
704                throws ParseException {
705                
706                Object value = getClaim(name);
707                
708                if (value == null || value instanceof Boolean) {
709                        return (Boolean)value;
710                } else {
711                        throw new ParseException("The \"" + name + "\" claim is not a Boolean", 0);
712                }
713        }
714
715
716        /**
717         * Gets the specified claim (registered or custom) as
718         * {@link java.lang.Integer}. May involve truncation if
719         * {@link Integer#MAX_VALUE} or {@link Integer#MIN_VALUE} is exceeded.
720         *
721         * @param name The name of the claim. Must not be {@code null}.
722         *
723         * @return The value of the claim, {@code null} if not specified.
724         *
725         * @throws ParseException If the claim value is not of the required
726         *                        type.
727         */
728        public Integer getIntegerClaim(final String name)
729                throws ParseException {
730                
731                Object value = getClaim(name);
732                
733                if (value == null) {
734                        return null;
735                } else if (value instanceof Number) {
736                        return ((Number)value).intValue();
737                } else {
738                        throw new ParseException("The \"" + name + "\" claim is not an Integer", 0);
739                }
740        }
741
742
743        /**
744         * Gets the specified claim (registered or custom) as
745         * {@link java.lang.Long}. May involve truncation if
746         * {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE} is exceeded.
747         *
748         * @param name The name of the claim. Must not be {@code null}.
749         *
750         * @return The value of the claim, {@code null} if not specified.
751         *
752         * @throws ParseException If the claim value is not of the required
753         *                        type.
754         */
755        public Long getLongClaim(final String name)
756                throws ParseException {
757                
758                Object value = getClaim(name);
759                
760                if (value == null) {
761                        return null;
762                } else if (value instanceof Number) {
763                        return ((Number)value).longValue();
764                } else {
765                        throw new ParseException("The \"" + name + "\" claim is not a Number", 0);
766                }
767        }
768
769
770        /**
771         * Gets the specified claim (registered or custom) as
772         * {@link java.util.Date}. The claim may be represented by a Date
773         * object or a number of a seconds since the Unix epoch.
774         *
775         * @param name The name of the claim. Must not be {@code null}.
776         *
777         * @return The value of the claim, {@code null} if not specified.
778         *
779         * @throws ParseException If the claim value is not of the required
780         *                        type.
781         */
782        public Date getDateClaim(final String name)
783                throws ParseException {
784
785                Object value = getClaim(name);
786
787                if (value == null) {
788                        return null;
789                } else if (value instanceof Date) {
790                        return (Date)value;
791                } else if (value instanceof Number) {
792                        return DateUtils.fromSecondsSinceEpoch(((Number)value).longValue());
793                } else {
794                        throw new ParseException("The \"" + name + "\" claim is not a Date", 0);
795                }
796        }
797
798
799        /**
800         * Gets the specified claim (registered or custom) as
801         * {@link java.lang.Float}. May involve truncation if {@link
802         * Float#MAX_VALUE} or {@link Float#MIN_VALUE} is exceeded, or
803         * rounding if floating-point precision is exceeded.
804         *
805         * @param name The name of the claim. Must not be {@code null}.
806         *
807         * @return The value of the claim, {@code null} if not specified.
808         *
809         * @throws ParseException If the claim value is not of the required
810         *                        type.
811         */
812        public Float getFloatClaim(final String name)
813                throws ParseException {
814                
815                Object value = getClaim(name);
816                
817                if (value == null) {
818                        return null;
819                } else if (value instanceof Number) {
820                        return ((Number)value).floatValue();
821                } else {
822                        throw new ParseException("The \"" + name + "\" claim is not a Float", 0);
823                }
824        }
825
826
827        /**
828         * Gets the specified claim (registered or custom) as
829         * {@link java.lang.Double}. May involve truncation if {@link
830         * Double#MAX_VALUE} or {@link Double#MIN_VALUE} is exceeded, or
831         * rounding if floating-point precision is exceeded.
832         *
833         * @param name The name of the claim. Must not be {@code null}.
834         *
835         * @return The value of the claim, {@code null} if not specified.
836         *
837         * @throws ParseException If the claim value is not of the required
838         *                        type.
839         */
840        public Double getDoubleClaim(final String name)
841                throws ParseException {
842                
843                Object value = getClaim(name);
844                
845                if (value == null) {
846                        return null;
847                } else if (value instanceof Number) {
848                        return ((Number)value).doubleValue();
849                } else {
850                        throw new ParseException("The \"" + name + "\" claim is not a Double", 0);
851                }
852        }
853
854
855        /**
856         * Gets the specified claim (registered or custom) as a JSON object.
857         *
858         * @param name The name of the claim. Must not be {@code null}.
859         *
860         * @return The value of the claim, {@code null} if not specified.
861         *
862         * @throws ParseException If the claim value is not of the required
863         *                        type.
864         */
865        public Map<String, Object> getJSONObjectClaim(final String name)
866                throws ParseException {
867
868                Object value = getClaim(name);
869
870                if (value == null) {
871                        return null;
872                } else if (value instanceof Map) {
873                        Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject();
874                        Map<?,?> map = (Map<?,?>)value;
875                        for (Map.Entry<?,?> entry: map.entrySet()) {
876                                if (entry.getKey() instanceof String) {
877                                        jsonObject.put((String)entry.getKey(), entry.getValue());
878                                }
879                        }
880                        return jsonObject;
881                } else {
882                        throw new ParseException("The \"" + name + "\" claim is not a JSON object or Map", 0);
883                }
884        }
885
886
887        /**
888         * Gets the claims (registered and custom).
889         *
890         * <p>Note that the registered claims Expiration-Time ({@code exp}),
891         * Not-Before-Time ({@code nbf}) and Issued-At ({@code iat}) will be
892         * returned as {@code java.util.Date} instances.
893         *
894         * @return The claims, as an unmodifiable map, empty map if none.
895         */
896        public Map<String,Object> getClaims() {
897
898                return Collections.unmodifiableMap(claims);
899        }
900        
901        
902        /**
903         * Returns a JOSE object payload representation of this claims set. The
904         * claims are serialised according to their insertion order. Claims
905         * with {@code null} values are output according to
906         * {@link Builder#serializeNullClaims(boolean)}.
907         *
908         * @return The payload representation.
909         */
910        public Payload toPayload() {
911                
912                return new Payload(toJSONObject(serializeNullClaims));
913        }
914
915
916        /**
917         * Returns a JOSE object payload representation of this claims set. The
918         * claims are serialised according to their insertion order.
919         *
920         * @param serializeNullClaims {@code true} to serialise claims with
921         *                            {@code null} values, {@code false} to
922         *                            omit them.
923         *
924         * @return The payload representation.
925         */
926        public Payload toPayload(final boolean serializeNullClaims) {
927
928                return new Payload(toJSONObject(serializeNullClaims));
929        }
930
931
932        /**
933         * Returns the JSON object representation of this claims set. The
934         * claims are serialised according to their insertion order. Claims
935         * with {@code null} values are output according to
936         * {@link Builder#serializeNullClaims(boolean)}.
937         *
938         * @return The JSON object representation.
939         */
940        public Map<String, Object> toJSONObject() {
941
942                return toJSONObject(serializeNullClaims);
943        }
944        
945        
946        /**
947         * Returns the JSON object representation of this claims set. The
948         * claims are serialised according to their insertion order.
949         *
950         * @param serializeNullClaims {@code true} to serialise claims with
951         *                            {@code null} values, {@code false} to
952         *                            omit them.
953         *
954         * @return The JSON object representation.
955         */
956        public Map<String, Object> toJSONObject(final boolean serializeNullClaims) {
957                
958                Map<String, Object> o = JSONObjectUtils.newJSONObject();
959                
960                for (Map.Entry<String,Object> claim: claims.entrySet()) {
961                        
962                        if (claim.getValue() instanceof Date) {
963                                
964                                // Transform dates to Unix timestamps
965                                Date dateValue = (Date) claim.getValue();
966                                o.put(claim.getKey(), DateUtils.toSecondsSinceEpoch(dateValue));
967                                
968                        } else if (JWTClaimNames.AUDIENCE.equals(claim.getKey())) {
969                                
970                                // Serialise single audience list and string
971                                List<String> audList = getAudience();
972                                
973                                if (audList != null && ! audList.isEmpty()) {
974                                        if (audList.size() == 1) {
975                                                o.put(JWTClaimNames.AUDIENCE, audList.get(0));
976                                        } else {
977                                                List<Object> audArray = JSONArrayUtils.newJSONArray();
978                                                audArray.addAll(audList);
979                                                o.put(JWTClaimNames.AUDIENCE, audArray);
980                                        }
981                                } else if (serializeNullClaims) {
982                                        o.put(JWTClaimNames.AUDIENCE, null);
983                                }
984                                
985                        } else if (claim.getValue() != null) {
986                                o.put(claim.getKey(), claim.getValue());
987                        } else if (serializeNullClaims) {
988                                o.put(claim.getKey(), null);
989                        }
990                }
991                
992                return o;
993        }
994        
995        
996        /**
997         * Returns a JSON object string representation of this claims set. The
998         * claims are serialised according to their insertion order. Claims
999         * with {@code null} values are output according to
1000         * {@link Builder#serializeNullClaims(boolean)}.
1001         *
1002         * @return The JSON object string representation.
1003         */
1004        @Override
1005        public String toString() {
1006
1007                return JSONObjectUtils.toJSONString(toJSONObject());
1008        }
1009        
1010        
1011        /**
1012         * Returns a JSON object string representation of this claims set. The
1013         * claims are serialised according to their insertion order.
1014         *
1015         * @param serializeNullClaims {@code true} to serialise claims with
1016         *                            {@code null} values, {@code false} to
1017         *                            omit them.
1018         *
1019         * @return The JSON object string representation.
1020         */
1021        public String toString(final boolean serializeNullClaims) {
1022
1023                return JSONObjectUtils.toJSONString(toJSONObject(serializeNullClaims));
1024        }
1025
1026        
1027        /**
1028         * Returns a transformation of this JWT claims set.
1029         *
1030         * @param <T> Type of the result.
1031         * @param transformer The JWT claims set transformer. Must not be
1032         *                    {@code null}.
1033         *
1034         * @return The transformed JWT claims set.
1035         */
1036        public <T> T toType(final JWTClaimsSetTransformer<T> transformer) {
1037
1038                return transformer.transform(this);
1039        }
1040
1041
1042        /**
1043         * Parses a JSON Web Token (JWT) claims set from the specified JSON
1044         * object representation.
1045         *
1046         * @param json The JSON object to parse. Must not be {@code null}.
1047         *
1048         * @return The JWT claims set.
1049         *
1050         * @throws ParseException If the specified JSON object doesn't 
1051         *                        represent a valid JWT claims set.
1052         */
1053        public static JWTClaimsSet parse(final Map<String, Object> json)
1054                throws ParseException {
1055
1056                JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
1057
1058                // Parse registered + custom params
1059                for (final String name: json.keySet()) {
1060                        
1061                        switch (name) {
1062                                case JWTClaimNames.ISSUER:
1063                                        builder.issuer(JSONObjectUtils.getString(json, JWTClaimNames.ISSUER));
1064                                        break;
1065                                case JWTClaimNames.SUBJECT:
1066                                        Object subValue = json.get(JWTClaimNames.SUBJECT);
1067                                        if (subValue instanceof String) {
1068                                                builder.subject(JSONObjectUtils.getString(json, JWTClaimNames.SUBJECT));
1069                                        } else if (subValue instanceof Number) {
1070                                                // Numbers not allowed per JWT spec, compromise
1071                                                // to enable interop with non-compliant libs
1072                                                // https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2
1073                                                builder.subject(String.valueOf(subValue));
1074                                        } else if (subValue == null) {
1075                                                builder.subject(null);
1076                                        } else {
1077                                                throw new ParseException("Illegal " + JWTClaimNames.SUBJECT + " claim", 0);
1078                                        }
1079                                        break;
1080                                case JWTClaimNames.AUDIENCE:
1081                                        Object audValue = json.get(JWTClaimNames.AUDIENCE);
1082                                        if (audValue instanceof String) {
1083                                                List<String> singleAud = new ArrayList<>();
1084                                                singleAud.add(JSONObjectUtils.getString(json, JWTClaimNames.AUDIENCE));
1085                                                builder.audience(singleAud);
1086                                        } else if (audValue instanceof List) {
1087                                                builder.audience(JSONObjectUtils.getStringList(json, JWTClaimNames.AUDIENCE));
1088                                        } else if (audValue == null) {
1089                                                builder.audience((String) null);
1090                                        } else {
1091                                                throw new ParseException("Illegal " + JWTClaimNames.AUDIENCE + " claim", 0);
1092                                        }
1093                                        break;
1094                                case JWTClaimNames.EXPIRATION_TIME:
1095                                        builder.expirationTime(JSONObjectUtils.getEpochSecondAsDate(json, JWTClaimNames.EXPIRATION_TIME));
1096                                        break;
1097                                case JWTClaimNames.NOT_BEFORE:
1098                                        builder.notBeforeTime(JSONObjectUtils.getEpochSecondAsDate(json, JWTClaimNames.NOT_BEFORE));
1099                                        break;
1100                                case JWTClaimNames.ISSUED_AT:
1101                                        builder.issueTime(JSONObjectUtils.getEpochSecondAsDate(json, JWTClaimNames.ISSUED_AT));
1102                                        break;
1103                                case JWTClaimNames.JWT_ID:
1104                                        builder.jwtID(JSONObjectUtils.getString(json, JWTClaimNames.JWT_ID));
1105                                        break;
1106                                default:
1107                                        builder.claim(name, json.get(name));
1108                                        break;
1109                        }
1110                }
1111
1112                return builder.build();
1113        }
1114
1115
1116        /**
1117         * Parses a JSON Web Token (JWT) claims set from the specified JSON
1118         * object string representation.
1119         *
1120         * @param s The JSON object string to parse. Must not be {@code null}.
1121         *
1122         * @return The JWT claims set.
1123         *
1124         * @throws ParseException If the specified JSON object string doesn't
1125         *                        represent a valid JWT claims set.
1126         */
1127        public static JWTClaimsSet parse(final String s)
1128                throws ParseException {
1129
1130                return parse(JSONObjectUtils.parse(s));
1131        }
1132
1133        
1134        @Override
1135        public boolean equals(Object o) {
1136                if (this == o) return true;
1137                if (!(o instanceof JWTClaimsSet)) return false;
1138                JWTClaimsSet that = (JWTClaimsSet) o;
1139                return Objects.equals(claims, that.claims);
1140        }
1141
1142        
1143        @Override
1144        public int hashCode() {
1145                return Objects.hash(claims);
1146        }
1147}