001    package com.nimbusds.jwt;
002    
003    
004    import java.text.ParseException;
005    import java.util.ArrayList;
006    import java.util.Collections;
007    import java.util.Date;
008    import java.util.HashMap;
009    import java.util.HashSet;
010    import java.util.List;
011    import java.util.Map;
012    import java.util.Set;
013    
014    import net.minidev.json.JSONArray;
015    import net.minidev.json.JSONObject;
016    
017    import 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-04-08)
043     */
044    public 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() / 1000);
529                    }
530    
531                    if (nbf != null) {
532                            o.put(NOT_BEFORE_CLAIM, nbf.getTime() / 1000);
533                    }
534    
535                    if (iat != null) {
536                            o.put(ISSUED_AT_CLAIM, iat.getTime() / 1000);
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 
560             *                        represent 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) * 1000));
594                            }
595                            else if (name.equals(NOT_BEFORE_CLAIM)) {
596    
597                                    cs.setNotBeforeTime(new Date(JSONObjectUtils.getLong(json, NOT_BEFORE_CLAIM) * 1000));
598                            }
599                            else if (name.equals(ISSUED_AT_CLAIM)) {
600    
601                                    cs.setIssueTime(new Date(JSONObjectUtils.getLong(json, ISSUED_AT_CLAIM) * 1000));
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    }