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