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