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