001package com.nimbusds.oauth2.sdk.auth;
002
003
004import java.util.Collections;
005import java.util.Date;
006import java.util.LinkedHashSet;
007import java.util.LinkedList;
008import java.util.List;
009import java.util.Set;
010
011import net.minidev.json.JSONObject;
012
013import com.nimbusds.jwt.JWTClaimsSet;
014import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
015
016import com.nimbusds.oauth2.sdk.ParseException;
017import com.nimbusds.oauth2.sdk.id.Audience;
018import com.nimbusds.oauth2.sdk.id.ClientID;
019import com.nimbusds.oauth2.sdk.id.Issuer;
020import com.nimbusds.oauth2.sdk.id.JWTID;
021import com.nimbusds.oauth2.sdk.id.Subject;
022import com.nimbusds.oauth2.sdk.util.DateUtils;
023import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
024
025
026/**
027 * JWT client authentication claims set, serialisable to a JSON object and JWT 
028 * claims set. This class is immutable.
029 *
030 * <p>Used for {@link ClientSecretJWT client secret JWT} and 
031 * {@link PrivateKeyJWT private key JWT} authentication at the Token endpoint.
032 *
033 * <p>Example client authentication claims set:
034 *
035 * <pre>
036 * {
037 *   "iss" : "http://client.example.com",
038 *   "sub" : "http://client.example.com",
039 *   "aud" : [ "http://idp.example.com/token" ],
040 *   "jti" : "d396036d-c4d9-40d8-8e98-f7e8327002d9",
041 *   "exp" : 1311281970,
042 *   "iat" : 1311280970
043 * }
044 * </pre>
045 *
046 * <p>Related specifications:
047 *
048 * <ul>
049 *     <li>OAuth 2.0 (RFC 6749), section-3.2.1.
050 *     <li>JSON Web Token (JWT) Bearer Token Profiles for OAuth 2.0 
051 *         (draft-ietf-oauth-jwt-bearer-06)
052 * </ul>
053 *
054 * @author Vladimir Dzhuvinov
055 */
056public class JWTAuthenticationClaimsSet {
057
058
059        /**
060         * The names of the reserved client authentication claims.
061         */
062        private static final Set<String> reservedClaimNames = new LinkedHashSet<String>();
063        
064        
065        static {
066                reservedClaimNames.add("iss");
067                reservedClaimNames.add("sub");
068                reservedClaimNames.add("aud");
069                reservedClaimNames.add("exp");
070                reservedClaimNames.add("nbf");
071                reservedClaimNames.add("iat");
072                reservedClaimNames.add("jti");
073        }
074        
075
076        /**
077         * Gets the names of the reserved client authentication claims.
078         *
079         * @return The names of the reserved client authentication claims 
080         *         (read-only set).
081         */
082        public static Set<String> getReservedClaimNames() {
083        
084                return Collections.unmodifiableSet(reservedClaimNames);
085        }
086        
087        
088        /**
089         * The issuer (required).
090         */
091        private final Issuer iss;
092        
093        
094        /**
095         * The subject (required).
096         */
097        private final Subject sub;
098        
099        
100        /**
101         * The audience that this token is intended for (required).
102         */
103        private final Audience aud;
104        
105        
106        /**
107         * The expiration time that limits the time window during which the JWT 
108         * can be used (required). The serialised value is number of seconds 
109         * from 1970-01-01T0:0:0Z as measured in UTC until the desired 
110         * date/time.
111         */
112        private final Date exp;
113
114
115        /**
116         * The time before which this token must not be accepted for 
117         * processing (optional). The serialised value is number of seconds 
118         * from 1970-01-01T0:0:0Z as measured in UTC until the desired 
119         * date/time.
120         */
121        private final Date nbf;
122        
123        
124        /**
125         * The time at which this token was issued (optional). The serialised
126         * value is number of seconds from 1970-01-01T0:0:0Z as measured in UTC 
127         * until the desired date/time.
128         */
129        private final Date iat;
130
131
132        /**
133         * Unique identifier for the JWT (optional). The JWT ID may be used by
134         * implementations requiring message de-duplication for one-time use 
135         * assertions. 
136         */
137        private final JWTID jti;
138        
139        
140        /**
141         * Creates a new JWT client authentication claims set.
142         *
143         * @param clientID The client identifier. Used to specify the issuer 
144         *                 and the subject. Must not be {@code null}.
145         * @param aud      The audience identifier, typically the URL of the 
146         *                 authorisation server's Token endpoint. Must not be 
147         *                 {@code null}.
148         * @param exp      The expiration time. Must not be {@code null}.
149         * @param nbf      The time before which the token must not be 
150         *                 accepted for processing, {@code null} if not
151         *                 specified.
152         * @param iat      The time at which the token was issued, 
153         *                 {@code null} if not specified.
154         * @param jti      Unique identifier for the JWT, {@code null} if
155         *                 not specified.
156         */
157        public JWTAuthenticationClaimsSet(final ClientID clientID,
158                                          final Audience aud,
159                                          final Date exp,
160                                          final Date nbf,
161                                          final Date iat,
162                                          final JWTID jti) {
163
164                if (clientID == null)
165                        throw new IllegalArgumentException("The client ID must not be null");
166
167                iss = new Issuer(clientID.getValue());
168
169                sub = new Subject(clientID.getValue());
170
171                
172                if (aud == null)
173                        throw new IllegalArgumentException("The audience must not be null");
174
175                this.aud = aud;
176
177
178                if (exp == null)
179                        throw new IllegalArgumentException("The expiration time must not be null");
180
181                this.exp = exp;
182
183
184                this.nbf = nbf;
185                this.iat = iat;
186                this.jti = jti;
187        }
188
189
190        /**
191         * Gets the client identifier. Corresponds to the {@code iss} and
192         * {@code sub} claims.
193         *
194         * @return The client identifier.
195         */
196        public ClientID getClientID() {
197
198                return new ClientID(iss.getValue());
199        }
200
201        
202        
203        /**
204         * Gets the issuer. Corresponds to the {@code iss} claim.
205         *
206         * @return The issuer. Contains the identifier of the OAuth client.
207         */
208        public Issuer getIssuer() {
209        
210                return iss;
211        }
212        
213        
214        /**
215         * Gets the subject. Corresponds to the {@code sub} claim.
216         *
217         * @return The subject. Contains the identifier of the OAuth client.
218         */
219        public Subject getSubject() {
220        
221                return sub;
222        }
223        
224        
225        /**
226         * Gets the audience. Corresponds to the {@code aud} claim 
227         * (single-valued).
228         *
229         * @return The audience, typically the URL of the authorisation 
230         *         server's token endpoint.
231         */
232        public Audience getAudience() {
233        
234                return aud;
235        }
236
237
238        /**
239         * Gets the expiration time. Corresponds to the {@code exp} claim.
240         *
241         * @return The expiration time.
242         */
243        public Date getExpirationTime() {
244        
245                return exp;
246        }
247        
248        
249        /**
250         * Gets the not-before time. Corresponds to the {@code nbf} claim.
251         *
252         * @return The not-before time, {@code null} if not specified.
253         */
254        public Date getNotBeforeTime() {
255        
256                return nbf;
257        }
258
259
260        /**
261         * Gets the optional issue time. Corresponds to the {@code iat} claim.
262         *
263         * @return The issued-at time, {@code null} if not specified.
264         */
265        public Date getIssueTime() {
266        
267                return iat;
268        }
269        
270        
271        /**
272         * Gets the identifier for the JWT. Corresponds to the {@code jti} 
273         * claim.
274         *
275         * @return The identifier for the JWT, {@code null} if not specified.
276         */
277        public JWTID getJWTID() {
278        
279                return jti;
280        }
281        
282        
283        /**
284         * Returns a JSON object representation of this JWT client 
285         * authentication claims set.
286         *
287         * @return The JSON object.
288         */
289        public JSONObject toJSONObject() {
290        
291                JSONObject o = new JSONObject();
292                
293                o.put("iss", iss.getValue());
294                o.put("sub", sub.getValue());
295
296                List<Object> audList = new LinkedList<Object>();
297                audList.add(aud);
298                o.put("aud", audList);
299
300                o.put("exp", DateUtils.toSecondsSinceEpoch(exp));
301
302                if (nbf != null)
303                        o.put("nbf", DateUtils.toSecondsSinceEpoch(nbf));
304                
305                if (iat != null)
306                        o.put("iat", DateUtils.toSecondsSinceEpoch(iat));
307                
308                if (jti != null)
309                        o.put("jti", jti.getValue());
310                
311                return o;
312        }
313
314
315        /**
316         * Returns a JSON Web Token (JWT) claims set representation of this
317         * client authentication claims set.
318         *
319         * @return The JWT claims set.
320         */
321        public JWTClaimsSet toJWTClaimsSet() {
322
323                JWTClaimsSet jwtClaimsSet = new JWTClaimsSet();
324
325                jwtClaimsSet.setIssuer(iss.getValue());
326                jwtClaimsSet.setSubject(sub.getValue());
327
328                List<String> audList = new LinkedList<String>();
329                audList.add(aud.getValue());
330
331                jwtClaimsSet.setAudience(audList);
332                jwtClaimsSet.setExpirationTime(exp);
333
334                if (nbf != null)
335                        jwtClaimsSet.setNotBeforeTime(nbf);
336                
337                if (iat != null)
338                        jwtClaimsSet.setIssueTime(iat);
339                
340                if (jti != null)
341                        jwtClaimsSet.setJWTID(jti.getValue());
342
343                return jwtClaimsSet;
344        }
345        
346        
347        /**
348         * Parses a JWT client authentication claims set from the specified 
349         * JSON object.
350         *
351         * @param jsonObject The JSON object. Must not be {@code null}.
352         *
353         * @return The client authentication claims set.
354         *
355         * @throws ParseException If the JSON object couldn't be parsed to a 
356         *                        client authentication claims set.
357         */
358        public static JWTAuthenticationClaimsSet parse(final JSONObject jsonObject)
359                throws ParseException {
360                
361                // Parse required claims
362                Issuer iss = new Issuer(JSONObjectUtils.getString(jsonObject, "iss"));
363                Subject sub = new Subject(JSONObjectUtils.getString(jsonObject, "sub"));
364
365                Audience aud;
366
367                if (jsonObject.get("aud") instanceof String) {
368
369                        aud = new Audience(JSONObjectUtils.getString(jsonObject, "aud"));
370
371                } else {
372                        String[] audList = JSONObjectUtils.getStringArray(jsonObject, "aud");
373
374                        if (audList.length > 1)
375                                throw new ParseException("Multiple audiences (aud) not supported");
376
377                        aud = new Audience(audList[0]);
378                }
379
380                Date exp = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "exp"));
381
382
383                // Parse optional claims
384
385                Date nbf = null;
386
387                if (jsonObject.containsKey("nbf"))
388                        nbf = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "nbf"));
389
390                Date iat = null;
391
392                if (jsonObject.containsKey("iat"))
393                        iat = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "iat"));
394
395                JWTID jti = null;
396
397                if (jsonObject.containsKey("jti"))
398                        jti = new JWTID(JSONObjectUtils.getString(jsonObject, "jti"));
399
400
401                // Check client ID
402
403                if (! iss.getValue().equals(sub.getValue()))
404                        throw new ParseException("JWT issuer and subject must have the same client ID");
405
406                ClientID clientID = new ClientID(iss.getValue());
407
408                return new JWTAuthenticationClaimsSet(clientID, aud, exp, nbf, iat, jti);
409        }
410
411
412        /**
413         * Parses a JWT client authentication claims set from the specified JWT 
414         * claims set.
415         *
416         * @param jwtClaimsSet The JWT claims set. Must not be {@code null}.
417         *
418         * @return The client authentication claims set.
419         *
420         * @throws ParseException If the JWT claims set couldn't be parsed to a 
421         *                        client authentication claims set.
422         */
423        public static JWTAuthenticationClaimsSet parse(final ReadOnlyJWTClaimsSet jwtClaimsSet)
424                throws ParseException {
425                
426                return parse(jwtClaimsSet.toJSONObject());
427        }
428}