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