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 carry {@link #setCustomClaims custom claims}; these will 
038 * be serialised and parsed along the reserved ones.
039 *
040 * @author Vladimir Dzhuvinov
041 * @author Justin Richer
042 * @version $version$ (2013-03-27)
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        /* (non-Javadoc)
158         * @see java.lang.Object#clone()
159         */
160        @Override
161        protected Object clone() throws CloneNotSupportedException {
162
163                // TODO Auto-generated method stub
164                return super.clone();
165        }
166
167
168        /**
169         * Gets the reserved JWT claim names.
170         *
171         * @return The reserved claim names, as an unmodifiable set.
172         */
173        public static Set<String> getReservedNames() {
174
175                return RESERVED_CLAIM_NAMES;
176        }
177
178
179        @Override
180        public String getIssuer() {
181
182                return iss;
183        }
184
185
186        /**
187         * Sets the issuer ({@code iss}) claim.
188         *
189         * @param iss The issuer claim, {@code null} if not specified.
190         */
191        public void setIssuer(final String iss) {
192
193                this.iss = iss;
194        }
195
196
197        @Override
198        public String getSubject() {
199
200                return sub;
201        }
202
203
204        /**
205         * Sets the subject ({@code sub}) claim.
206         *
207         * @param sub The subject claim, {@code null} if not specified.
208         */
209        public void setSubject(final String sub) {
210
211                this.sub = sub;
212        }
213
214
215        @Override
216        public List<String> getAudience() {
217
218                return aud;
219        }
220
221
222        /**
223         * Sets the audience ({@code aud}) clam.
224         *
225         * @param aud The audience claim, {@code null} if not specified.
226         */
227        public void setAudience(final List<String> aud) {
228
229                this.aud = aud;
230        }
231
232
233        @Override
234        public Date getExpirationTime() {
235
236                return exp;
237        }
238
239
240        /**
241         * Sets the expiration time ({@code exp}) claim.
242         *
243         * @param exp The expiration time, {@code null} if not specified.
244         */
245        public void setExpirationTime(final Date exp) {
246
247                this.exp = exp;
248        }
249
250
251        @Override
252        public Date getNotBeforeTime() {
253
254                return nbf;
255        }
256
257
258        /**
259         * Sets the not-before ({@code nbf}) claim.
260         *
261         * @param nbf The not-before claim, {@code null} if not specified.
262         */
263        public void setNotBeforeTime(final Date nbf) {
264
265                this.nbf = nbf;
266        }
267
268
269        @Override
270        public Date getIssueTime() {
271
272                return iat;
273        }
274
275
276        /**
277         * Sets the issued-at ({@code iat}) claim.
278         *
279         * @param iat The issued-at claim, {@code null} if not specified.
280         */
281        public void setIssueTime(final Date iat) {
282
283                this.iat = iat;
284        }
285
286
287        @Override
288        public String getJWTID() {
289
290                return jti;
291        }
292
293
294        /**
295         * Sets the JWT ID ({@code jti}) claim.
296         *
297         * @param jti The JWT ID claim, {@code null} if not specified.
298         */
299        public void setJWTID(final String jti) {
300
301                this.jti = jti;
302        }
303
304
305        @Override
306        public String getType() {
307
308                return typ;
309        }
310
311
312        /**
313         * Sets the type ({@code typ}) claim.
314         *
315         * @param typ The type claim, {@code null} if not specified.
316         */
317        public void setType(final String typ) {
318
319                this.typ = typ;
320        }
321
322
323        @Override
324        public Object getCustomClaim(final String name) {
325
326                return customClaims.get(name);
327        }
328
329
330        /**
331         * Sets a custom (non-reserved) claim.
332         *
333         * @param name  The name of the custom claim. Must not be {@code null}.
334         * @param value The value of the custom claim, should map to a valid 
335         *              JSON entity, {@code null} if not specified.
336         *
337         * @throws IllegalArgumentException If the specified custom claim name
338         *                                  matches a reserved claim name.
339         */
340        public void setCustomClaim(final String name, final Object value) {
341
342                if (getReservedNames().contains(name)) {
343
344                        throw new IllegalArgumentException("The claim name \"" + name + "\" matches a reserved name");
345                }
346
347                customClaims.put(name, value);
348        }
349
350
351        @Override 
352        public Map<String,Object> getCustomClaims() {
353
354                return Collections.unmodifiableMap(customClaims);
355        }
356
357
358        /**
359         * Sets the custom (non-reserved) claims. The values must be 
360         * serialisable to a JSON entity, otherwise will be ignored.
361         *
362         * @param customClaims The custom claims, empty map or {@code null} if
363         *                     none.
364         */
365        public void setCustomClaims(final Map<String,Object> customClaims) {
366
367                if (customClaims == null) {
368                        return;
369                }
370
371                this.customClaims = customClaims;
372        }
373
374
375        @Override
376        public Object getClaim(final String name) {
377
378                if (!getReservedNames().contains(name)) {
379
380                        return getCustomClaim(name);
381
382                } else {
383                        // it's a reserved name, find out which one
384                        if (ISSUER_CLAIM.equals(name)) {
385                                return getIssuer();
386                        } else if (SUBJECT_CLAIM.equals(name)) {
387                                return getSubject();
388                        } else if (AUDIENCE_CLAIM.equals(name)) {
389                                return getAudience();
390                        } else if (EXPIRATION_TIME_CLAIM.equals(name)) {
391                                return getExpirationTime();
392                        } else if (NOT_BEFORE_CLAIM.equals(name)) {
393                                return getNotBeforeTime();
394                        } else if (ISSUED_AT_CLAIM.equals(name)) {
395                                return getIssueTime();
396                        } else if (JWT_ID_CLAIM.equals(name)) {
397                                return getJWTID();
398                        } else if (TYPE_CLAIM.equals(name)) {
399                                return getType();
400                        } else {
401                                // if we fall through down to here, something is wrong
402                                throw new IllegalArgumentException("Couldn't find reserved claim: " + name);
403                        }
404                }
405        }
406
407
408        /**
409         * Sets the specified claim, whether reserved or custom.
410         *
411         * @param name  The name of the claim to set. Must not be {@code null}.
412         * @param value The value of the claim to set. May be {@code null}.
413         *
414         * @throws IllegalArgumentException If the claim is reserved and its
415         *                                  value is not of the expected type.
416         */
417        public void setClaim(final String name, final Object value) {
418
419                if (!getReservedNames().contains(name)) {
420                        setCustomClaim(name, value);
421                } else {
422                        // it's a reserved name, find out which one
423                        if (ISSUER_CLAIM.equals(name)) {
424                                if (value instanceof String) {
425                                        setIssuer((String)value);
426                                } else {
427                                        throw new IllegalArgumentException("Issuer claim must be a String");
428                                }
429                        } else if (SUBJECT_CLAIM.equals(name)) {
430                                if (value instanceof String) {
431                                        setSubject((String)value);
432                                } else {
433                                        throw new IllegalArgumentException("Subject claim must be a String");
434                                }
435                        } else if (AUDIENCE_CLAIM.equals(name)) {
436                                if (value instanceof List<?>) {
437                                        setAudience((List<String>)value);
438                                } else {
439                                        throw new IllegalArgumentException("Audience claim must be a List<String>");
440                                }
441                        } else if (EXPIRATION_TIME_CLAIM.equals(name)) {
442                                if (value instanceof Date) {
443                                        setExpirationTime((Date)value);
444                                } else {
445                                        throw new IllegalArgumentException("Expiration claim must be a Date");
446                                }
447                        } else if (NOT_BEFORE_CLAIM.equals(name)) {
448                                if (value instanceof Date) {
449                                        setNotBeforeTime((Date)value);
450                                } else {
451                                        throw new IllegalArgumentException("Not-before claim must be a Date");
452                                }
453                        } else if (ISSUED_AT_CLAIM.equals(name)) {
454                                if (value instanceof Date) {
455                                        setIssueTime((Date)value);
456                                } else {
457                                        throw new IllegalArgumentException("Issued-at claim must be a Date");
458                                }
459                        } else if (JWT_ID_CLAIM.equals(name)) {
460                                if (value instanceof String) {
461                                        setJWTID((String)value);
462                                } else {
463                                        throw new IllegalArgumentException("JWT-ID claim must be a String");
464                                }
465                        } else if (TYPE_CLAIM.equals(name)) {
466                                if (value instanceof String) {
467                                        setType((String)value);
468                                } else {
469                                        throw new IllegalArgumentException("Type claim must be a String");
470                                }
471                        } else {
472                                // if we fall through down to here, something is wrong
473                                throw new IllegalArgumentException("Couldn't find reserved claim: " + name);
474                        }
475                }
476        }
477
478
479        @Override
480        public Map<String, Object> getAllClaims() {
481
482                Map<String, Object> allClaims = new HashMap<String, Object>();
483
484                allClaims.putAll(customClaims);
485
486                for (String reservedClaim : RESERVED_CLAIM_NAMES) {
487
488                        allClaims.put(reservedClaim, getClaim(reservedClaim));
489                }
490
491                return Collections.unmodifiableMap(allClaims);
492        }
493
494
495        /** 
496         * Sets the claims of this JWT claims set, replacing any existing ones.
497         *
498         * @param newClaims The JWT claims. Must not be {@code null}.
499         */
500        public void setAllClaims(final Map<String, Object> newClaims) {
501
502                for (String name : newClaims.keySet()) {
503                        setClaim(name, newClaims.get(name));
504                }
505        }
506
507
508        @Override
509        public JSONObject toJSONObject() {
510
511                JSONObject o = new JSONObject(customClaims);
512
513                if (iss != null) {
514                        o.put(ISSUER_CLAIM, iss);
515                }
516
517                if (sub != null) {
518                        o.put(SUBJECT_CLAIM, sub);
519                }
520
521                if (aud != null) {
522                        JSONArray audArray = new JSONArray();
523                        audArray.addAll(aud);
524                        o.put(AUDIENCE_CLAIM, audArray);
525                }
526
527                if (exp != null) {
528                        o.put(EXPIRATION_TIME_CLAIM, exp.getTime());
529                }
530
531                if (nbf != null) {
532                        o.put(NOT_BEFORE_CLAIM, nbf.getTime());
533                }
534
535                if (iat != null) {
536                        o.put(ISSUED_AT_CLAIM, iat.getTime());
537                }
538
539                if (jti != null) {
540                        o.put(JWT_ID_CLAIM, jti);
541                }
542
543                if (typ != null) {
544                        o.put(TYPE_CLAIM, typ);
545                }
546
547                return o;
548        }
549
550
551        /**
552         * Parses a JSON Web Token (JWT) claims set from the specified
553         * JSON object representation.
554         *
555         * @param json The JSON object to parse. Must not be {@code null}.
556         *
557         * @return The JWT claims set.
558         *
559         * @throws ParseException If the specified JSON object doesn't represent
560         *                        a valid JWT claims set.
561         */
562        public static JWTClaimsSet parse(final JSONObject json)
563                        throws ParseException {
564
565                JWTClaimsSet cs = new JWTClaimsSet();
566
567                // Parse reserved + custom params
568                for (final String name: json.keySet()) {
569
570                        if (name.equals(ISSUER_CLAIM)) {
571
572                                cs.setIssuer(JSONObjectUtils.getString(json, ISSUER_CLAIM));
573                        }
574                        else if (name.equals(SUBJECT_CLAIM)) {
575
576                                cs.setSubject(JSONObjectUtils.getString(json, SUBJECT_CLAIM));
577                        }
578                        else if (name.equals(AUDIENCE_CLAIM)) {
579
580                                Object audValue = json.get(AUDIENCE_CLAIM);
581
582                                if (audValue != null && audValue instanceof String) {
583                                        List<String> singleAud = new ArrayList<String>();
584                                        singleAud.add(JSONObjectUtils.getString(json, AUDIENCE_CLAIM));
585                                        cs.setAudience(singleAud);
586                                }
587                                else {
588                                        cs.setAudience(JSONObjectUtils.getStringList(json, AUDIENCE_CLAIM));
589                                }
590                        }
591                        else if (name.equals(EXPIRATION_TIME_CLAIM)) {
592
593                                cs.setExpirationTime(new Date(JSONObjectUtils.getLong(json, EXPIRATION_TIME_CLAIM)));
594                        }
595                        else if (name.equals(NOT_BEFORE_CLAIM)) {
596
597                                cs.setNotBeforeTime(new Date(JSONObjectUtils.getLong(json, NOT_BEFORE_CLAIM)));
598                        }
599                        else if (name.equals(ISSUED_AT_CLAIM)) {
600
601                                cs.setIssueTime(new Date(JSONObjectUtils.getLong(json, ISSUED_AT_CLAIM)));
602                        }
603                        else if (name.equals(JWT_ID_CLAIM)) {
604
605                                cs.setJWTID(JSONObjectUtils.getString(json, JWT_ID_CLAIM));
606                        }
607                        else if (name.equals(TYPE_CLAIM)) {
608
609                                cs.setType(JSONObjectUtils.getString(json, TYPE_CLAIM));
610                        }
611                        else {
612                                cs.setCustomClaim(name, json.get(name));
613                        }
614                }
615
616                return cs;
617        }
618
619
620        /* (non-Javadoc)
621         * @see java.lang.Object#toString()
622         */
623        @Override
624        public String toString() {
625
626                return "JWTClaimsSet [iss=" + iss + ", sub=" + sub + ", aud=" + aud + ", exp=" + exp + ", nbf=" + nbf + ", iat=" + iat + ", jti=" + jti + ", typ=" + typ + ", customClaims=" + customClaims + "]";
627        }
628}