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