001package com.nimbusds.jwt;
002
003
004import java.text.ParseException;
005import java.util.ArrayList;
006import java.util.Collections;
007import java.util.Date;
008import java.util.HashMap;
009import java.util.HashSet;
010import java.util.List;
011import java.util.Map;
012import java.util.Set;
013
014import net.minidev.json.JSONArray;
015import net.minidev.json.JSONObject;
016
017import com.nimbusds.jose.util.JSONObjectUtils;
018
019
020/**
021 * JSON Web Token (JWT) claims set.
022 *
023 * <p>Supports all {@link #getReservedNames reserved claims} of the JWT 
024 * specification:
025 *
026 * <ul>
027 *     <li>iss - Issuer
028 *     <li>sub - Subject
029 *     <li>aud - Audience
030 *     <li>exp - Expiration Time
031 *     <li>nbf - Not Before
032 *     <li>iat - Issued At
033 *     <li>jti - JWT ID
034 *     <li>typ - Type
035 * </ul>
036 *
037 * <p>The set may also contain {@link #setCustomClaims custom claims}; these 
038 * will be serialised and parsed along the reserved ones.
039 *
040 * @author Vladimir Dzhuvinov
041 * @author Justin Richer
042 * @version $version$ (2013-07-26)
043 */
044public class JWTClaimsSet implements ReadOnlyJWTClaimsSet {
045
046
047        private static final String TYPE_CLAIM = "typ";
048        private static final String JWT_ID_CLAIM = "jti";
049        private static final String ISSUED_AT_CLAIM = "iat";
050        private static final String NOT_BEFORE_CLAIM = "nbf";
051        private static final String EXPIRATION_TIME_CLAIM = "exp";
052        private static final String AUDIENCE_CLAIM = "aud";
053        private static final String SUBJECT_CLAIM = "sub";
054        private static final String ISSUER_CLAIM = "iss";
055
056
057        /**
058         * The reserved claim names.
059         */
060        private static final Set<String> RESERVED_CLAIM_NAMES;
061
062
063        /**
064         * Initialises the reserved claim name set.
065         */
066        static {
067                Set<String> n = new HashSet<String>();
068
069                n.add(ISSUER_CLAIM);
070                n.add(SUBJECT_CLAIM);
071                n.add(AUDIENCE_CLAIM);
072                n.add(EXPIRATION_TIME_CLAIM);
073                n.add(NOT_BEFORE_CLAIM);
074                n.add(ISSUED_AT_CLAIM);
075                n.add(JWT_ID_CLAIM);
076                n.add(TYPE_CLAIM);
077
078                RESERVED_CLAIM_NAMES = Collections.unmodifiableSet(n);
079        }
080
081
082        /**
083         * The issuer claim.
084         */
085        private String iss = null;
086
087
088        /**
089         * The subject claim.
090         */
091        private String sub = null;
092
093
094        /**
095         * The audience claim.
096         */
097        private List<String> aud = null;
098
099
100        /**
101         * The expiration time claim.
102         */
103        private Date exp = null;
104
105
106        /**
107         * The not-before claim.
108         */
109        private Date nbf = null;
110
111
112        /**
113         * The issued-at claim.
114         */
115        private Date iat = null;
116
117
118        /**
119         * The JWT ID claim.
120         */
121        private String jti = null;
122
123
124        /**
125         * The type claim.
126         */
127        private String typ = null;
128
129
130        /**
131         * Custom claims.
132         */
133        private Map<String,Object> customClaims = new HashMap<String,Object>();
134
135
136        /**
137         * Creates a new empty JWT claims set.
138         */
139        public JWTClaimsSet() {
140
141                // Nothing to do
142        }
143
144
145        /**
146         * Creates a copy of the specified JWT claims set.
147         *
148         * @param old The JWT claims set to copy. Must not be {@code null}.
149         */
150        public JWTClaimsSet(final ReadOnlyJWTClaimsSet old) {
151                
152                super();
153                setAllClaims(old.getAllClaims());
154        }
155
156
157        @Override
158        protected Object clone() throws CloneNotSupportedException {
159
160                // TODO Auto-generated method stub
161                return super.clone();
162        }
163
164
165        /**
166         * Gets the reserved JWT claim names.
167         *
168         * @return The reserved claim names, as a unmodifiable set.
169         */
170        public static Set<String> getReservedNames() {
171
172                return RESERVED_CLAIM_NAMES;
173        }
174
175
176        @Override
177        public String getIssuer() {
178
179                return iss;
180        }
181
182
183        /**
184         * Sets the issuer ({@code iss}) claim.
185         *
186         * @param iss The issuer claim, {@code null} if not specified.
187         */
188        public void setIssuer(final String iss) {
189
190                this.iss = iss;
191        }
192
193
194        @Override
195        public String getSubject() {
196
197                return sub;
198        }
199
200
201        /**
202         * Sets the subject ({@code sub}) claim.
203         *
204         * @param sub The subject claim, {@code null} if not specified.
205         */
206        public void setSubject(final String sub) {
207
208                this.sub = sub;
209        }
210
211
212        @Override
213        public List<String> getAudience() {
214
215                return aud;
216        }
217
218
219        /**
220         * Sets the audience ({@code aud}) claim.
221         *
222         * @param aud The audience claim, {@code null} if not specified.
223         */
224        public void setAudience(final List<String> aud) {
225
226                this.aud = aud;
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-reserved) 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 reserved claim name.
336         */
337        public void setCustomClaim(final String name, final Object value) {
338
339                if (getReservedNames().contains(name)) {
340
341                        throw new IllegalArgumentException("The claim name \"" + name + "\" matches a reserved 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-reserved) claims. If a claim value doesn't map
357         * 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 Boolean getBooleanClaim(final String name)
413                throws ParseException {
414                
415                Object value = getClaim(name);
416                
417                if (value == null || value instanceof Boolean) {
418                        return (Boolean)value;
419                } else {
420                        throw new ParseException("The \"" + name + "\" claim is not a Boolean", 0);
421                }
422        }
423        
424        
425        @Override
426        public Integer getIntegerClaim(final String name)
427                throws ParseException {
428                
429                Object value = getClaim(name);
430                
431                if (value == null) {
432                        return null;
433                } else if (value instanceof Number) {
434                        return ((Number)value).intValue();
435                } else {
436                        throw new ParseException("The \"" + name + "\" claim is not an Integer", 0);
437                }
438        }
439        
440        
441        @Override
442        public Long getLongClaim(final String name)
443                throws ParseException {
444                
445                Object value = getClaim(name);
446                
447                if (value == null) {
448                        return null;
449                } else if (value instanceof Number) {
450                        return ((Number)value).longValue();
451                } else {
452                        throw new ParseException("The \"" + name + "\" claim is not a Number", 0);
453                }
454        }
455        
456        
457        @Override
458        public Float getFloatClaim(final String name)
459                throws ParseException {
460                
461                Object value = getClaim(name);
462                
463                if (value == null) {
464                        return null;
465                } else if (value instanceof Number) {
466                        return ((Number)value).floatValue();
467                } else {
468                        throw new ParseException("The \"" + name + "\" claim is not a Float", 0);
469                }
470        }
471        
472        
473        @Override
474        public Double getDoubleClaim(final String name)
475                throws ParseException {
476                
477                Object value = getClaim(name);
478                
479                if (value == null) {
480                        return null;
481                } else if (value instanceof Number) {
482                        return ((Number)value).doubleValue();
483                } else {
484                        throw new ParseException("The \"" + name + "\" claim is not a Double", 0);
485                }
486        }
487
488
489        /**
490         * Sets the specified claim, whether reserved or custom.
491         *
492         * @param name  The name of the claim to set. Must not be {@code null}.
493         * @param value The value of the claim to set, {@code null} if not 
494         *              specified.
495         *
496         * @throws IllegalArgumentException If the claim is reserved and its
497         *                                  value is not of the expected type.
498         */
499        public void setClaim(final String name, final Object value) {
500
501                if (ISSUER_CLAIM.equals(name)) {
502                        if (value == null || value instanceof String) {
503                                setIssuer((String) value);
504                        } else {
505                                throw new IllegalArgumentException("Issuer claim must be a String");
506                        }
507                } else if (SUBJECT_CLAIM.equals(name)) {
508                        if (value == null || value instanceof String) {
509                                setSubject((String) value);
510                        } else {
511                                throw new IllegalArgumentException("Subject claim must be a String");
512                        }
513                } else if (AUDIENCE_CLAIM.equals(name)) {
514                        if (value == null || value instanceof List<?>) {
515                                setAudience((List<String>) value);
516                        } else {
517                                throw new IllegalArgumentException("Audience claim must be a List<String>");
518                        }
519                } else if (EXPIRATION_TIME_CLAIM.equals(name)) {
520                        if (value == null || value instanceof Date) {
521                                setExpirationTime((Date) value);
522                        } else {
523                                throw new IllegalArgumentException("Expiration claim must be a Date");
524                        }
525                } else if (NOT_BEFORE_CLAIM.equals(name)) {
526                        if (value == null || value instanceof Date) {
527                                setNotBeforeTime((Date) value);
528                        } else {
529                                throw new IllegalArgumentException("Not-before claim must be a Date");
530                        }
531                } else if (ISSUED_AT_CLAIM.equals(name)) {
532                        if (value == null || value instanceof Date) {
533                                setIssueTime((Date) value);
534                        } else {
535                                throw new IllegalArgumentException("Issued-at claim must be a Date");
536                        }
537                } else if (JWT_ID_CLAIM.equals(name)) {
538                        if (value == null || value instanceof String) {
539                                setJWTID((String) value);
540                        } else {
541                                throw new IllegalArgumentException("JWT-ID claim must be a String");
542                        }
543                } else if (TYPE_CLAIM.equals(name)) {
544                        if (value == null || value instanceof String) {
545                                setType((String) value);
546                        } else {
547                                throw new IllegalArgumentException("Type claim must be a String");
548                        }
549                } else {
550                        setCustomClaim(name, value);
551                }
552        }
553
554
555        @Override
556        public Map<String, Object> getAllClaims() {
557
558                Map<String, Object> allClaims = new HashMap<String, Object>();
559
560                allClaims.putAll(customClaims);
561
562                for (String reservedClaim : RESERVED_CLAIM_NAMES) {
563
564                        allClaims.put(reservedClaim, getClaim(reservedClaim));
565                }
566
567                return Collections.unmodifiableMap(allClaims);
568        }
569
570
571        /** 
572         * Sets the claims of this JWT claims set, replacing any existing ones.
573         *
574         * @param newClaims The JWT claims. Must not be {@code null}.
575         */
576        public void setAllClaims(final Map<String, Object> newClaims) {
577
578                for (String name : newClaims.keySet()) {
579                        setClaim(name, newClaims.get(name));
580                }
581        }
582
583
584        @Override
585        public JSONObject toJSONObject() {
586
587                JSONObject o = new JSONObject(customClaims);
588
589                if (iss != null) {
590                        o.put(ISSUER_CLAIM, iss);
591                }
592
593                if (sub != null) {
594                        o.put(SUBJECT_CLAIM, sub);
595                }
596
597                if (aud != null) {
598                        JSONArray audArray = new JSONArray();
599                        audArray.addAll(aud);
600                        o.put(AUDIENCE_CLAIM, audArray);
601                }
602
603                if (exp != null) {
604                        o.put(EXPIRATION_TIME_CLAIM, exp.getTime() / 1000);
605                }
606
607                if (nbf != null) {
608                        o.put(NOT_BEFORE_CLAIM, nbf.getTime() / 1000);
609                }
610
611                if (iat != null) {
612                        o.put(ISSUED_AT_CLAIM, iat.getTime() / 1000);
613                }
614
615                if (jti != null) {
616                        o.put(JWT_ID_CLAIM, jti);
617                }
618
619                if (typ != null) {
620                        o.put(TYPE_CLAIM, typ);
621                }
622
623                return o;
624        }
625
626
627        /**
628         * Parses a JSON Web Token (JWT) claims set from the specified JSON
629         * object representation.
630         *
631         * @param json The JSON object to parse. Must not be {@code null}.
632         *
633         * @return The JWT claims set.
634         *
635         * @throws ParseException If the specified JSON object doesn't 
636         *                        represent a valid JWT claims set.
637         */
638        public static JWTClaimsSet parse(final JSONObject json)
639                        throws ParseException {
640
641                JWTClaimsSet cs = new JWTClaimsSet();
642
643                // Parse reserved + custom params
644                for (final String name: json.keySet()) {
645
646                        if (name.equals(ISSUER_CLAIM)) {
647
648                                cs.setIssuer(JSONObjectUtils.getString(json, ISSUER_CLAIM));
649
650                        } else if (name.equals(SUBJECT_CLAIM)) {
651
652                                cs.setSubject(JSONObjectUtils.getString(json, SUBJECT_CLAIM));
653
654                        } else if (name.equals(AUDIENCE_CLAIM)) {
655
656                                Object audValue = json.get(AUDIENCE_CLAIM);
657
658                                if (audValue instanceof String) {
659                                        List<String> singleAud = new ArrayList<String>();
660                                        singleAud.add(JSONObjectUtils.getString(json, AUDIENCE_CLAIM));
661                                        cs.setAudience(singleAud);
662                                } else if (audValue instanceof List) {
663                                        cs.setAudience(JSONObjectUtils.getStringList(json, AUDIENCE_CLAIM));
664                                }
665
666                        } else if (name.equals(EXPIRATION_TIME_CLAIM)) {
667
668                                cs.setExpirationTime(new Date(JSONObjectUtils.getLong(json, EXPIRATION_TIME_CLAIM) * 1000));
669
670                        } else if (name.equals(NOT_BEFORE_CLAIM)) {
671
672                                cs.setNotBeforeTime(new Date(JSONObjectUtils.getLong(json, NOT_BEFORE_CLAIM) * 1000));
673
674                        } else if (name.equals(ISSUED_AT_CLAIM)) {
675
676                                cs.setIssueTime(new Date(JSONObjectUtils.getLong(json, ISSUED_AT_CLAIM) * 1000));
677
678                        } else if (name.equals(JWT_ID_CLAIM)) {
679
680                                cs.setJWTID(JSONObjectUtils.getString(json, JWT_ID_CLAIM));
681
682                        } else if (name.equals(TYPE_CLAIM)) {
683
684                                cs.setType(JSONObjectUtils.getString(json, TYPE_CLAIM));
685
686                        } else {
687                                cs.setCustomClaim(name, json.get(name));
688                        }
689                }
690
691                return cs;
692        }
693
694
695        /**
696         * Parses a JSON Web Token (JWT) claims set from the specified JSON
697         * object string representation.
698         *
699         * @param s The JSON object string to parse. Must not be {@code null}.
700         *
701         * @return The JWT claims set.
702         *
703         * @throws ParseException If the specified JSON object string doesn't
704         *                        represent a valid JWT claims set.
705         */
706        public static JWTClaimsSet parse(final String s)
707                throws ParseException {
708
709                return parse(JSONObjectUtils.parseJSONObject(s));
710        }
711
712        @Override
713        public String toString() {
714
715                return "JWTClaimsSet [iss=" + iss + ", sub=" + sub + ", aud=" + aud + ", exp=" + exp + ", nbf=" + nbf + ", iat=" + iat + ", jti=" + jti + ", typ=" + typ + ", customClaims=" + customClaims + "]";
716        }
717}