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