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