001package com.nimbusds.jwt;
002
003
004import java.text.ParseException;
005import java.util.*;
006
007import net.minidev.json.JSONArray;
008import net.minidev.json.JSONObject;
009
010import com.nimbusds.jose.util.JSONObjectUtils;
011
012
013/**
014 * JSON Web Token (JWT) claims set.
015 *
016 * <p>Supports all {@link #getRegisteredNames()}  registered claims} of the JWT
017 * specification:
018 *
019 * <ul>
020 *     <li>iss - Issuer
021 *     <li>sub - Subject
022 *     <li>aud - Audience
023 *     <li>exp - Expiration Time
024 *     <li>nbf - Not Before
025 *     <li>iat - Issued At
026 *     <li>jti - JWT ID
027 *     <li>typ - Type
028 * </ul>
029 *
030 * <p>The set may also contain {@link #setCustomClaims custom claims}; these 
031 * will be serialised and parsed along the registered ones.
032 *
033 * @author Vladimir Dzhuvinov
034 * @author Justin Richer
035 * @version $version$ (2014-08-16)
036 */
037public class JWTClaimsSet implements ReadOnlyJWTClaimsSet {
038
039
040        private static final String TYPE_CLAIM = "typ";
041        private static final String JWT_ID_CLAIM = "jti";
042        private static final String ISSUED_AT_CLAIM = "iat";
043        private static final String NOT_BEFORE_CLAIM = "nbf";
044        private static final String EXPIRATION_TIME_CLAIM = "exp";
045        private static final String AUDIENCE_CLAIM = "aud";
046        private static final String SUBJECT_CLAIM = "sub";
047        private static final String ISSUER_CLAIM = "iss";
048
049
050        /**
051         * The registered claim names.
052         */
053        private static final Set<String> REGISTERED_CLAIM_NAMES;
054
055
056        /**
057         * Initialises the registered claim name set.
058         */
059        static {
060                Set<String> n = new HashSet<>();
061
062                n.add(ISSUER_CLAIM);
063                n.add(SUBJECT_CLAIM);
064                n.add(AUDIENCE_CLAIM);
065                n.add(EXPIRATION_TIME_CLAIM);
066                n.add(NOT_BEFORE_CLAIM);
067                n.add(ISSUED_AT_CLAIM);
068                n.add(JWT_ID_CLAIM);
069                n.add(TYPE_CLAIM);
070
071                REGISTERED_CLAIM_NAMES = Collections.unmodifiableSet(n);
072        }
073
074
075        /**
076         * The issuer claim.
077         */
078        private String iss = null;
079
080
081        /**
082         * The subject claim.
083         */
084        private String sub = null;
085
086
087        /**
088         * The audience claim.
089         */
090        private List<String> aud = null;
091
092
093        /**
094         * The expiration time claim.
095         */
096        private Date exp = null;
097
098
099        /**
100         * The not-before claim.
101         */
102        private Date nbf = null;
103
104
105        /**
106         * The issued-at claim.
107         */
108        private Date iat = null;
109
110
111        /**
112         * The JWT ID claim.
113         */
114        private String jti = null;
115
116
117        /**
118         * The type claim.
119         */
120        private String typ = null;
121
122
123        /**
124         * Custom claims.
125         */
126        private Map<String,Object> customClaims = new HashMap<>();
127
128
129        /**
130         * Creates a new empty JWT claims set.
131         */
132        public JWTClaimsSet() {
133
134                // Nothing to do
135        }
136
137
138        /**
139         * Creates a copy of the specified JWT claims set.
140         *
141         * @param old The JWT claims set to copy. Must not be {@code null}.
142         */
143        public JWTClaimsSet(final ReadOnlyJWTClaimsSet old) {
144                
145                super();
146                setAllClaims(old.getAllClaims());
147        }
148
149
150        /**
151         * Gets the registered JWT claim names.
152         *
153         * @return The registered claim names, as a unmodifiable set.
154         */
155        public static Set<String> getRegisteredNames() {
156
157                return REGISTERED_CLAIM_NAMES;
158        }
159
160
161        @Override
162        public String getIssuer() {
163
164                return iss;
165        }
166
167
168        /**
169         * Sets the issuer ({@code iss}) claim.
170         *
171         * @param iss The issuer claim, {@code null} if not specified.
172         */
173        public void setIssuer(final String iss) {
174
175                this.iss = iss;
176        }
177
178
179        @Override
180        public String getSubject() {
181
182                return sub;
183        }
184
185
186        /**
187         * Sets the subject ({@code sub}) claim.
188         *
189         * @param sub The subject claim, {@code null} if not specified.
190         */
191        public void setSubject(final String sub) {
192
193                this.sub = sub;
194        }
195
196
197        @Override
198        public List<String> getAudience() {
199
200                return aud;
201        }
202
203
204        /**
205         * Sets the audience ({@code aud}) claim.
206         *
207         * @param aud The audience claim, {@code null} if not specified.
208         */
209        public void setAudience(final List<String> aud) {
210
211                this.aud = aud;
212        }
213
214
215        /**
216         * Sets a single-valued audience ({@code aud}) claim.
217         *
218         * @param aud The audience claim, {@code null} if not specified.
219         */
220        public void setAudience(final String aud) {
221
222                if (aud == null) {
223                        this.aud = null;
224                } else {
225                        this.aud = Arrays.asList(aud);
226                }
227        }
228
229
230        @Override
231        public Date getExpirationTime() {
232
233                return exp;
234        }
235
236
237        /**
238         * Sets the expiration time ({@code exp}) claim.
239         *
240         * @param exp The expiration time, {@code null} if not specified.
241         */
242        public void setExpirationTime(final Date exp) {
243
244                this.exp = exp;
245        }
246
247
248        @Override
249        public Date getNotBeforeTime() {
250
251                return nbf;
252        }
253
254
255        /**
256         * Sets the not-before ({@code nbf}) claim.
257         *
258         * @param nbf The not-before claim, {@code null} if not specified.
259         */
260        public void setNotBeforeTime(final Date nbf) {
261
262                this.nbf = nbf;
263        }
264
265
266        @Override
267        public Date getIssueTime() {
268
269                return iat;
270        }
271
272
273        /**
274         * Sets the issued-at ({@code iat}) claim.
275         *
276         * @param iat The issued-at claim, {@code null} if not specified.
277         */
278        public void setIssueTime(final Date iat) {
279
280                this.iat = iat;
281        }
282
283
284        @Override
285        public String getJWTID() {
286
287                return jti;
288        }
289
290
291        /**
292         * Sets the JWT ID ({@code jti}) claim.
293         *
294         * @param jti The JWT ID claim, {@code null} if not specified.
295         */
296        public void setJWTID(final String jti) {
297
298                this.jti = jti;
299        }
300
301
302        @Override
303        public String getType() {
304
305                return typ;
306        }
307
308
309        /**
310         * Sets the type ({@code typ}) claim.
311         *
312         * @param typ The type claim, {@code null} if not specified.
313         */
314        public void setType(final String typ) {
315
316                this.typ = typ;
317        }
318
319
320        @Override
321        public Object getCustomClaim(final String name) {
322
323                return customClaims.get(name);
324        }
325
326
327        /**
328         * Sets a custom (non-registered) claim.
329         *
330         * @param name  The name of the custom claim. Must not be {@code null}.
331         * @param value The value of the custom claim, should map to a valid 
332         *              JSON entity, {@code null} if not specified.
333         *
334         * @throws IllegalArgumentException If the specified custom claim name
335         *                                  matches a registered claim name.
336         */
337        public void setCustomClaim(final String name, final Object value) {
338
339                if (getRegisteredNames().contains(name)) {
340
341                        throw new IllegalArgumentException("The claim name \"" + name + "\" matches a registered name");
342                }
343
344                customClaims.put(name, value);
345        }
346
347
348        @Override 
349        public Map<String,Object> getCustomClaims() {
350
351                return Collections.unmodifiableMap(customClaims);
352        }
353
354
355        /**
356         * Sets the custom (non-registered) claims. If a claim value doesn't
357         * map to a JSON entity it will be ignored during serialisation.
358         *
359         * @param customClaims The custom claims, empty map or {@code null} if
360         *                     none.
361         */
362        public void setCustomClaims(final Map<String,Object> customClaims) {
363
364                if (customClaims == null) {
365                        this.customClaims.clear();
366                } else {
367                        this.customClaims = customClaims;
368                }
369        }
370
371
372        @Override
373        public Object getClaim(final String name) {
374
375                if(ISSUER_CLAIM.equals(name)) {
376                        return getIssuer();
377                } else if(SUBJECT_CLAIM.equals(name)) {
378                        return getSubject();
379                } else if(AUDIENCE_CLAIM.equals(name)) {
380                        return getAudience();
381                } else if(EXPIRATION_TIME_CLAIM.equals(name)) {
382                        return getExpirationTime();
383                } else if(NOT_BEFORE_CLAIM.equals(name)) {
384                        return getNotBeforeTime();
385                } else if(ISSUED_AT_CLAIM.equals(name)) {
386                        return getIssueTime();
387                } else if(JWT_ID_CLAIM.equals(name)) {
388                        return getJWTID();
389                } else if(TYPE_CLAIM.equals(name)) {
390                        return getType();
391                } else {
392                        return getCustomClaim(name);
393                }
394        }
395        
396        
397        @Override
398        public String getStringClaim(final String name)
399                throws ParseException {
400                
401                Object value = getClaim(name);
402                
403                if (value == null || value instanceof String) {
404                        return (String)value;
405                } else {
406                        throw new ParseException("The \"" + name + "\" claim is not a String", 0);
407                }
408        }
409
410
411        @Override
412        public String[] getStringArrayClaim(final String name)
413                throws ParseException {
414
415                Object value = getClaim(name);
416
417                if (value == null) {
418                        return null;
419                }
420
421                List<?> list;
422
423                try {
424                        list = (List)getClaim(name);
425
426                } catch (ClassCastException e) {
427                        throw new ParseException("The \"" + name + "\" claim is not a list / JSON array", 0);
428                }
429
430                String[] stringArray = new String[list.size()];
431
432                for (int i=0; i < stringArray.length; i++) {
433
434                        try {
435                                stringArray[i] = (String)list.get(i);
436                        } catch (ClassCastException e) {
437                                throw new ParseException("The \"" + name + "\" claim is not a list / JSON array of strings", 0);
438                        }
439                }
440
441                return stringArray;
442        }
443
444
445        public List<String> getStringListClaim(final String name)
446                throws ParseException {
447
448                String[] stringArray = getStringArrayClaim(name);
449
450                if (stringArray == null) {
451                        return null;
452                }
453
454                return Arrays.asList(stringArray);
455        }
456        
457        
458        @Override
459        public Boolean getBooleanClaim(final String name)
460                throws ParseException {
461                
462                Object value = getClaim(name);
463                
464                if (value == null || value instanceof Boolean) {
465                        return (Boolean)value;
466                } else {
467                        throw new ParseException("The \"" + name + "\" claim is not a Boolean", 0);
468                }
469        }
470        
471        
472        @Override
473        public Integer getIntegerClaim(final String name)
474                throws ParseException {
475                
476                Object value = getClaim(name);
477                
478                if (value == null) {
479                        return null;
480                } else if (value instanceof Number) {
481                        return ((Number)value).intValue();
482                } else {
483                        throw new ParseException("The \"" + name + "\" claim is not an Integer", 0);
484                }
485        }
486        
487        
488        @Override
489        public Long getLongClaim(final String name)
490                throws ParseException {
491                
492                Object value = getClaim(name);
493                
494                if (value == null) {
495                        return null;
496                } else if (value instanceof Number) {
497                        return ((Number)value).longValue();
498                } else {
499                        throw new ParseException("The \"" + name + "\" claim is not a Number", 0);
500                }
501        }
502        
503        
504        @Override
505        public Float getFloatClaim(final String name)
506                throws ParseException {
507                
508                Object value = getClaim(name);
509                
510                if (value == null) {
511                        return null;
512                } else if (value instanceof Number) {
513                        return ((Number)value).floatValue();
514                } else {
515                        throw new ParseException("The \"" + name + "\" claim is not a Float", 0);
516                }
517        }
518        
519        
520        @Override
521        public Double getDoubleClaim(final String name)
522                throws ParseException {
523                
524                Object value = getClaim(name);
525                
526                if (value == null) {
527                        return null;
528                } else if (value instanceof Number) {
529                        return ((Number)value).doubleValue();
530                } else {
531                        throw new ParseException("The \"" + name + "\" claim is not a Double", 0);
532                }
533        }
534
535
536        /**
537         * Sets the specified claim, whether registered or custom.
538         *
539         * @param name  The name of the claim to set. Must not be {@code null}.
540         * @param value The value of the claim to set, {@code null} if not 
541         *              specified.
542         *
543         * @throws IllegalArgumentException If the claim is registered and its
544         *                                  value is not of the expected type.
545         */
546        public void setClaim(final String name, final Object value) {
547
548                if (ISSUER_CLAIM.equals(name)) {
549                        if (value == null || value instanceof String) {
550                                setIssuer((String) value);
551                        } else {
552                                throw new IllegalArgumentException("Issuer claim must be a String");
553                        }
554                } else if (SUBJECT_CLAIM.equals(name)) {
555                        if (value == null || value instanceof String) {
556                                setSubject((String) value);
557                        } else {
558                                throw new IllegalArgumentException("Subject claim must be a String");
559                        }
560                } else if (AUDIENCE_CLAIM.equals(name)) {
561                        if (value == null || value instanceof List<?>) {
562                                setAudience((List<String>) value);
563                        } else {
564                                throw new IllegalArgumentException("Audience claim must be a List<String>");
565                        }
566                } else if (EXPIRATION_TIME_CLAIM.equals(name)) {
567                        if (value == null || value instanceof Date) {
568                                setExpirationTime((Date) value);
569                        } else {
570                                throw new IllegalArgumentException("Expiration claim must be a Date");
571                        }
572                } else if (NOT_BEFORE_CLAIM.equals(name)) {
573                        if (value == null || value instanceof Date) {
574                                setNotBeforeTime((Date) value);
575                        } else {
576                                throw new IllegalArgumentException("Not-before claim must be a Date");
577                        }
578                } else if (ISSUED_AT_CLAIM.equals(name)) {
579                        if (value == null || value instanceof Date) {
580                                setIssueTime((Date) value);
581                        } else {
582                                throw new IllegalArgumentException("Issued-at claim must be a Date");
583                        }
584                } else if (JWT_ID_CLAIM.equals(name)) {
585                        if (value == null || value instanceof String) {
586                                setJWTID((String) value);
587                        } else {
588                                throw new IllegalArgumentException("JWT-ID claim must be a String");
589                        }
590                } else if (TYPE_CLAIM.equals(name)) {
591                        if (value == null || value instanceof String) {
592                                setType((String) value);
593                        } else {
594                                throw new IllegalArgumentException("Type claim must be a String");
595                        }
596                } else {
597                        setCustomClaim(name, value);
598                }
599        }
600
601
602        @Override
603        public Map<String,Object> getAllClaims() {
604
605                Map<String, Object> allClaims = new HashMap<>();
606
607                allClaims.putAll(customClaims);
608
609                for (String registeredClaim : REGISTERED_CLAIM_NAMES) {
610
611                        Object value = getClaim(registeredClaim);
612
613                        if (value != null) {
614                                allClaims.put(registeredClaim, value);
615                        }
616                }
617
618                return Collections.unmodifiableMap(allClaims);
619        }
620
621
622        /** 
623         * Sets the claims of this JWT claims set, replacing any existing ones.
624         *
625         * @param newClaims The JWT claims. Must not be {@code null}.
626         */
627        public void setAllClaims(final Map<String, Object> newClaims) {
628
629                for (String name : newClaims.keySet()) {
630                        setClaim(name, newClaims.get(name));
631                }
632        }
633
634
635        @Override
636        public JSONObject toJSONObject() {
637
638                JSONObject o = new JSONObject(customClaims);
639
640                if (iss != null) {
641                        o.put(ISSUER_CLAIM, iss);
642                }
643
644                if (sub != null) {
645                        o.put(SUBJECT_CLAIM, sub);
646                }
647
648                if (aud != null && ! aud.isEmpty()) {
649
650                        if (aud.size() == 1) {
651                                o.put(AUDIENCE_CLAIM, aud.get(0));
652                        } else {
653                                JSONArray audArray = new JSONArray();
654                                audArray.addAll(aud);
655                                o.put(AUDIENCE_CLAIM, audArray);
656                        }
657                }
658
659                if (exp != null) {
660                        o.put(EXPIRATION_TIME_CLAIM, exp.getTime() / 1000);
661                }
662
663                if (nbf != null) {
664                        o.put(NOT_BEFORE_CLAIM, nbf.getTime() / 1000);
665                }
666
667                if (iat != null) {
668                        o.put(ISSUED_AT_CLAIM, iat.getTime() / 1000);
669                }
670
671                if (jti != null) {
672                        o.put(JWT_ID_CLAIM, jti);
673                }
674
675                if (typ != null) {
676                        o.put(TYPE_CLAIM, typ);
677                }
678
679                return o;
680        }
681
682
683        /**
684         * Parses a JSON Web Token (JWT) claims set from the specified JSON
685         * object representation.
686         *
687         * @param json The JSON object to parse. Must not be {@code null}.
688         *
689         * @return The JWT claims set.
690         *
691         * @throws ParseException If the specified JSON object doesn't 
692         *                        represent a valid JWT claims set.
693         */
694        public static JWTClaimsSet parse(final JSONObject json)
695                throws ParseException {
696
697                JWTClaimsSet cs = new JWTClaimsSet();
698
699                // Parse registered + custom params
700                for (final String name: json.keySet()) {
701
702                        if (name.equals(ISSUER_CLAIM)) {
703
704                                cs.setIssuer(JSONObjectUtils.getString(json, ISSUER_CLAIM));
705
706                        } else if (name.equals(SUBJECT_CLAIM)) {
707
708                                cs.setSubject(JSONObjectUtils.getString(json, SUBJECT_CLAIM));
709
710                        } else if (name.equals(AUDIENCE_CLAIM)) {
711
712                                Object audValue = json.get(AUDIENCE_CLAIM);
713
714                                if (audValue instanceof String) {
715                                        List<String> singleAud = new ArrayList<>();
716                                        singleAud.add(JSONObjectUtils.getString(json, AUDIENCE_CLAIM));
717                                        cs.setAudience(singleAud);
718                                } else if (audValue instanceof List) {
719                                        cs.setAudience(JSONObjectUtils.getStringList(json, AUDIENCE_CLAIM));
720                                }
721
722                        } else if (name.equals(EXPIRATION_TIME_CLAIM)) {
723
724                                cs.setExpirationTime(new Date(JSONObjectUtils.getLong(json, EXPIRATION_TIME_CLAIM) * 1000));
725
726                        } else if (name.equals(NOT_BEFORE_CLAIM)) {
727
728                                cs.setNotBeforeTime(new Date(JSONObjectUtils.getLong(json, NOT_BEFORE_CLAIM) * 1000));
729
730                        } else if (name.equals(ISSUED_AT_CLAIM)) {
731
732                                cs.setIssueTime(new Date(JSONObjectUtils.getLong(json, ISSUED_AT_CLAIM) * 1000));
733
734                        } else if (name.equals(JWT_ID_CLAIM)) {
735
736                                cs.setJWTID(JSONObjectUtils.getString(json, JWT_ID_CLAIM));
737
738                        } else if (name.equals(TYPE_CLAIM)) {
739
740                                cs.setType(JSONObjectUtils.getString(json, TYPE_CLAIM));
741
742                        } else {
743                                cs.setCustomClaim(name, json.get(name));
744                        }
745                }
746
747                return cs;
748        }
749
750
751        /**
752         * Parses a JSON Web Token (JWT) claims set from the specified JSON
753         * object string representation.
754         *
755         * @param s The JSON object string to parse. Must not be {@code null}.
756         *
757         * @return The JWT claims set.
758         *
759         * @throws ParseException If the specified JSON object string doesn't
760         *                        represent a valid JWT claims set.
761         */
762        public static JWTClaimsSet parse(final String s)
763                throws ParseException {
764
765                return parse(JSONObjectUtils.parseJSONObject(s));
766        }
767
768        @Override
769        public String toString() {
770
771                return "JWTClaimsSet [iss=" + iss + ", sub=" + sub + ", aud=" + aud + ", exp=" + exp + ", nbf=" + nbf + ", iat=" + iat + ", jti=" + jti + ", typ=" + typ + ", customClaims=" + customClaims + "]";
772        }
773}