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