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