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