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