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 2015-05-08
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 final Map<String,Object> customClaims = new LinkedHashMap<>();
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                if (aud == null) {
192                        return null;
193                }
194
195                return Collections.unmodifiableList(aud);
196        }
197
198
199        /**
200         * Sets the audience ({@code aud}) claim.
201         *
202         * @param aud The audience claim, {@code null} if not specified.
203         */
204        public void setAudience(final List<String> aud) {
205
206                this.aud = aud;
207        }
208
209
210        /**
211         * Sets a single-valued audience ({@code aud}) claim.
212         *
213         * @param aud The audience claim, {@code null} if not specified.
214         */
215        public void setAudience(final String aud) {
216
217                if (aud == null) {
218                        this.aud = null;
219                } else {
220                        this.aud = Arrays.asList(aud);
221                }
222        }
223
224
225        @Override
226        public Date getExpirationTime() {
227
228                return exp;
229        }
230
231
232        /**
233         * Sets the expiration time ({@code exp}) claim.
234         *
235         * @param exp The expiration time, {@code null} if not specified.
236         */
237        public void setExpirationTime(final Date exp) {
238
239                this.exp = exp;
240        }
241
242
243        @Override
244        public Date getNotBeforeTime() {
245
246                return nbf;
247        }
248
249
250        /**
251         * Sets the not-before ({@code nbf}) claim.
252         *
253         * @param nbf The not-before claim, {@code null} if not specified.
254         */
255        public void setNotBeforeTime(final Date nbf) {
256
257                this.nbf = nbf;
258        }
259
260
261        @Override
262        public Date getIssueTime() {
263
264                return iat;
265        }
266
267
268        /**
269         * Sets the issued-at ({@code iat}) claim.
270         *
271         * @param iat The issued-at claim, {@code null} if not specified.
272         */
273        public void setIssueTime(final Date iat) {
274
275                this.iat = iat;
276        }
277
278
279        @Override
280        public String getJWTID() {
281
282                return jti;
283        }
284
285
286        /**
287         * Sets the JWT ID ({@code jti}) claim.
288         *
289         * @param jti The JWT ID claim, {@code null} if not specified.
290         */
291        public void setJWTID(final String jti) {
292
293                this.jti = jti;
294        }
295
296
297        @Override
298        public Object getCustomClaim(final String name) {
299
300                return customClaims.get(name);
301        }
302
303
304        /**
305         * Sets a custom (non-registered) claim.
306         *
307         * @param name  The name of the custom claim. Must not be {@code null}.
308         * @param value The value of the custom claim, should map to a valid 
309         *              JSON entity, {@code null} if not specified.
310         *
311         * @throws IllegalArgumentException If the specified custom claim name
312         *                                  matches a registered claim name.
313         */
314        public void setCustomClaim(final String name, final Object value) {
315
316                if (getRegisteredNames().contains(name)) {
317
318                        throw new IllegalArgumentException("The claim name \"" + name + "\" matches a registered name");
319                }
320
321                customClaims.put(name, value);
322        }
323
324
325        @Override 
326        public Map<String,Object> getCustomClaims() {
327
328                return Collections.unmodifiableMap(customClaims);
329        }
330
331
332        /**
333         * Sets the custom (non-registered) claims. If a claim value doesn't
334         * map to a JSON entity it will be ignored during serialisation.
335         *
336         * @param customClaims The custom claims, empty map or {@code null} if
337         *                     none.
338         */
339        public void setCustomClaims(final Map<String,Object> customClaims) {
340
341                this.customClaims.clear();
342                
343                if(customClaims != null) {
344                        this.customClaims.putAll(customClaims); 
345                }
346        }
347
348
349        @Override
350        public Object getClaim(final String name) {
351
352                if(ISSUER_CLAIM.equals(name)) {
353                        return getIssuer();
354                } else if(SUBJECT_CLAIM.equals(name)) {
355                        return getSubject();
356                } else if(AUDIENCE_CLAIM.equals(name)) {
357                        return getAudience();
358                } else if(EXPIRATION_TIME_CLAIM.equals(name)) {
359                        return getExpirationTime();
360                } else if(NOT_BEFORE_CLAIM.equals(name)) {
361                        return getNotBeforeTime();
362                } else if(ISSUED_AT_CLAIM.equals(name)) {
363                        return getIssueTime();
364                } else if(JWT_ID_CLAIM.equals(name)) {
365                        return getJWTID();
366                } else {
367                        return getCustomClaim(name);
368                }
369        }
370        
371        
372        @Override
373        public String getStringClaim(final String name)
374                throws ParseException {
375                
376                Object value = getClaim(name);
377                
378                if (value == null || value instanceof String) {
379                        return (String)value;
380                } else {
381                        throw new ParseException("The \"" + name + "\" claim is not a String", 0);
382                }
383        }
384
385
386        @Override
387        public String[] getStringArrayClaim(final String name)
388                throws ParseException {
389
390                Object value = getClaim(name);
391
392                if (value == null) {
393                        return null;
394                }
395
396                List<?> list;
397
398                try {
399                        list = (List)getClaim(name);
400
401                } catch (ClassCastException e) {
402                        throw new ParseException("The \"" + name + "\" claim is not a list / JSON array", 0);
403                }
404
405                String[] stringArray = new String[list.size()];
406
407                for (int i=0; i < stringArray.length; i++) {
408
409                        try {
410                                stringArray[i] = (String)list.get(i);
411                        } catch (ClassCastException e) {
412                                throw new ParseException("The \"" + name + "\" claim is not a list / JSON array of strings", 0);
413                        }
414                }
415
416                return stringArray;
417        }
418
419
420        public List<String> getStringListClaim(final String name)
421                throws ParseException {
422
423                String[] stringArray = getStringArrayClaim(name);
424
425                if (stringArray == null) {
426                        return null;
427                }
428
429                return Collections.unmodifiableList(Arrays.asList(stringArray));
430        }
431        
432        
433        @Override
434        public Boolean getBooleanClaim(final String name)
435                throws ParseException {
436                
437                Object value = getClaim(name);
438                
439                if (value == null || value instanceof Boolean) {
440                        return (Boolean)value;
441                } else {
442                        throw new ParseException("The \"" + name + "\" claim is not a Boolean", 0);
443                }
444        }
445        
446        
447        @Override
448        public Integer getIntegerClaim(final String name)
449                throws ParseException {
450                
451                Object value = getClaim(name);
452                
453                if (value == null) {
454                        return null;
455                } else if (value instanceof Number) {
456                        return ((Number)value).intValue();
457                } else {
458                        throw new ParseException("The \"" + name + "\" claim is not an Integer", 0);
459                }
460        }
461        
462        
463        @Override
464        public Long getLongClaim(final String name)
465                throws ParseException {
466                
467                Object value = getClaim(name);
468                
469                if (value == null) {
470                        return null;
471                } else if (value instanceof Number) {
472                        return ((Number)value).longValue();
473                } else {
474                        throw new ParseException("The \"" + name + "\" claim is not a Number", 0);
475                }
476        }
477        
478        
479        @Override
480        public Float getFloatClaim(final String name)
481                throws ParseException {
482                
483                Object value = getClaim(name);
484                
485                if (value == null) {
486                        return null;
487                } else if (value instanceof Number) {
488                        return ((Number)value).floatValue();
489                } else {
490                        throw new ParseException("The \"" + name + "\" claim is not a Float", 0);
491                }
492        }
493        
494        
495        @Override
496        public Double getDoubleClaim(final String name)
497                throws ParseException {
498                
499                Object value = getClaim(name);
500                
501                if (value == null) {
502                        return null;
503                } else if (value instanceof Number) {
504                        return ((Number)value).doubleValue();
505                } else {
506                        throw new ParseException("The \"" + name + "\" claim is not a Double", 0);
507                }
508        }
509
510
511        /**
512         * Sets the specified claim, whether registered or custom.
513         *
514         * @param name  The name of the claim to set. Must not be {@code null}.
515         * @param value The value of the claim to set, {@code null} if not 
516         *              specified.
517         *
518         * @throws IllegalArgumentException If the claim is registered and its
519         *                                  value is not of the expected type.
520         */
521        public void setClaim(final String name, final Object value) {
522
523                if (ISSUER_CLAIM.equals(name)) {
524                        if (value == null || value instanceof String) {
525                                setIssuer((String) value);
526                        } else {
527                                throw new IllegalArgumentException("Issuer claim must be a String");
528                        }
529                } else if (SUBJECT_CLAIM.equals(name)) {
530                        if (value == null || value instanceof String) {
531                                setSubject((String) value);
532                        } else {
533                                throw new IllegalArgumentException("Subject claim must be a String");
534                        }
535                } else if (AUDIENCE_CLAIM.equals(name)) {
536                        if (value == null || value instanceof List<?>) {
537                                setAudience((List<String>) value);
538                        } else {
539                                throw new IllegalArgumentException("Audience claim must be a List<String>");
540                        }
541                } else if (EXPIRATION_TIME_CLAIM.equals(name)) {
542                        if (value == null || value instanceof Date) {
543                                setExpirationTime((Date) value);
544                        } else {
545                                throw new IllegalArgumentException("Expiration claim must be a Date");
546                        }
547                } else if (NOT_BEFORE_CLAIM.equals(name)) {
548                        if (value == null || value instanceof Date) {
549                                setNotBeforeTime((Date) value);
550                        } else {
551                                throw new IllegalArgumentException("Not-before claim must be a Date");
552                        }
553                } else if (ISSUED_AT_CLAIM.equals(name)) {
554                        if (value == null || value instanceof Date) {
555                                setIssueTime((Date) value);
556                        } else {
557                                throw new IllegalArgumentException("Issued-at claim must be a Date");
558                        }
559                } else if (JWT_ID_CLAIM.equals(name)) {
560                        if (value == null || value instanceof String) {
561                                setJWTID((String) value);
562                        } else {
563                                throw new IllegalArgumentException("JWT-ID claim must be a String");
564                        }
565                } else {
566                        setCustomClaim(name, value);
567                }
568        }
569
570
571        @Override
572        public Map<String,Object> getAllClaims() {
573
574                Map<String, Object> allClaims = new HashMap<>();
575
576                allClaims.putAll(customClaims);
577
578                for (String registeredClaim : REGISTERED_CLAIM_NAMES) {
579
580                        Object value = getClaim(registeredClaim);
581
582                        if (value != null) {
583                                allClaims.put(registeredClaim, value);
584                        }
585                }
586
587                return Collections.unmodifiableMap(allClaims);
588        }
589
590
591        /** 
592         * Sets the claims of this JWT claims set, replacing any existing ones.
593         *
594         * @param newClaims The JWT claims. Must not be {@code null}.
595         */
596        public void setAllClaims(final Map<String, Object> newClaims) {
597
598                for (String name : newClaims.keySet()) {
599                        setClaim(name, newClaims.get(name));
600                }
601        }
602
603
604        @Override
605        public JSONObject toJSONObject() {
606
607                JSONObject o = new JSONObject(customClaims);
608
609                if (iss != null) {
610                        o.put(ISSUER_CLAIM, iss);
611                }
612
613                if (sub != null) {
614                        o.put(SUBJECT_CLAIM, sub);
615                }
616
617                if (aud != null && ! aud.isEmpty()) {
618
619                        if (aud.size() == 1) {
620                                o.put(AUDIENCE_CLAIM, aud.get(0));
621                        } else {
622                                JSONArray audArray = new JSONArray();
623                                audArray.addAll(aud);
624                                o.put(AUDIENCE_CLAIM, audArray);
625                        }
626                }
627
628                if (exp != null) {
629                        o.put(EXPIRATION_TIME_CLAIM, exp.getTime() / 1000);
630                }
631
632                if (nbf != null) {
633                        o.put(NOT_BEFORE_CLAIM, nbf.getTime() / 1000);
634                }
635
636                if (iat != null) {
637                        o.put(ISSUED_AT_CLAIM, iat.getTime() / 1000);
638                }
639
640                if (jti != null) {
641                        o.put(JWT_ID_CLAIM, jti);
642                }
643
644                return o;
645        }
646
647
648        /**
649         * Parses a JSON Web Token (JWT) claims set from the specified JSON
650         * object representation.
651         *
652         * @param json The JSON object to parse. Must not be {@code null}.
653         *
654         * @return The JWT claims set.
655         *
656         * @throws ParseException If the specified JSON object doesn't 
657         *                        represent a valid JWT claims set.
658         */
659        public static JWTClaimsSet parse(final JSONObject json)
660                throws ParseException {
661
662                JWTClaimsSet cs = new JWTClaimsSet();
663
664                // Parse registered + custom params
665                for (final String name: json.keySet()) {
666
667                        if (name.equals(ISSUER_CLAIM)) {
668
669                                cs.setIssuer(JSONObjectUtils.getString(json, ISSUER_CLAIM));
670
671                        } else if (name.equals(SUBJECT_CLAIM)) {
672
673                                cs.setSubject(JSONObjectUtils.getString(json, SUBJECT_CLAIM));
674
675                        } else if (name.equals(AUDIENCE_CLAIM)) {
676
677                                Object audValue = json.get(AUDIENCE_CLAIM);
678
679                                if (audValue instanceof String) {
680                                        List<String> singleAud = new ArrayList<>();
681                                        singleAud.add(JSONObjectUtils.getString(json, AUDIENCE_CLAIM));
682                                        cs.setAudience(singleAud);
683                                } else if (audValue instanceof List) {
684                                        cs.setAudience(JSONObjectUtils.getStringList(json, AUDIENCE_CLAIM));
685                                }
686
687                        } else if (name.equals(EXPIRATION_TIME_CLAIM)) {
688
689                                cs.setExpirationTime(new Date(JSONObjectUtils.getLong(json, EXPIRATION_TIME_CLAIM) * 1000));
690
691                        } else if (name.equals(NOT_BEFORE_CLAIM)) {
692
693                                cs.setNotBeforeTime(new Date(JSONObjectUtils.getLong(json, NOT_BEFORE_CLAIM) * 1000));
694
695                        } else if (name.equals(ISSUED_AT_CLAIM)) {
696
697                                cs.setIssueTime(new Date(JSONObjectUtils.getLong(json, ISSUED_AT_CLAIM) * 1000));
698
699                        } else if (name.equals(JWT_ID_CLAIM)) {
700
701                                cs.setJWTID(JSONObjectUtils.getString(json, JWT_ID_CLAIM));
702
703                        } else {
704                                cs.setCustomClaim(name, json.get(name));
705                        }
706                }
707
708                return cs;
709        }
710
711
712        /**
713         * Parses a JSON Web Token (JWT) claims set from the specified JSON
714         * object string representation.
715         *
716         * @param s The JSON object string to parse. Must not be {@code null}.
717         *
718         * @return The JWT claims set.
719         *
720         * @throws ParseException If the specified JSON object string doesn't
721         *                        represent a valid JWT claims set.
722         */
723        public static JWTClaimsSet parse(final String s)
724                throws ParseException {
725
726                return parse(JSONObjectUtils.parseJSONObject(s));
727        }
728
729        @Override
730        public String toString() {
731
732                return "JWTClaimsSet [iss=" + iss + ", sub=" + sub + ", aud=" + aud + ", exp=" + exp + ", nbf=" + nbf + ", iat=" + iat + ", jti=" + jti + ", customClaims=" + customClaims + "]";
733        }
734}