001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk.assertions.jwt;
019
020
021import java.util.*;
022
023import net.jcip.annotations.Immutable;
024import net.minidev.json.JSONObject;
025
026import com.nimbusds.jwt.JWTClaimsSet;
027import com.nimbusds.jwt.util.DateUtils;
028import com.nimbusds.oauth2.sdk.ParseException;
029import com.nimbusds.oauth2.sdk.assertions.AssertionDetails;
030import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
031import com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT;
032import com.nimbusds.oauth2.sdk.id.*;
033import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
034
035
036/**
037 * JSON Web Token (JWT) bearer assertion details (claims set) for OAuth 2.0
038 * client authentication and authorisation grants.
039 *
040 * <p>Used for {@link ClientSecretJWT client secret JWT} and
041 * {@link PrivateKeyJWT private key JWT} authentication at the Token endpoint
042 * as well as {@link com.nimbusds.oauth2.sdk.JWTBearerGrant JWT bearer
043 * assertion grants}.
044 *
045 * <p>Example JWT bearer assertion claims set for client authentication:
046 *
047 * <pre>
048 * {
049 *   "iss" : "https://client.example.com",
050 *   "sub" : "https://client.example.com",
051 *   "aud" : "https://server.c2id.com",
052 *   "jti" : "d396036d-c4d9-40d8-8e98-f7e8327002d9",
053 *   "exp" : 1311281970,
054 *   "iat" : 1311280970
055 * }
056 * </pre>
057 *
058 * <p>Related specifications:
059 *
060 * <ul>
061 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
062 *         Authorization Grants (RFC 7523)
063 * </ul>
064 */
065@Immutable
066public class JWTAssertionDetails extends AssertionDetails {
067
068
069        /**
070         * The names of the reserved JWT claims.
071         */
072        private static final Set<String> reservedClaimsNames = new LinkedHashSet<>();
073
074
075        static {
076                reservedClaimsNames.add("iss");
077                reservedClaimsNames.add("sub");
078                reservedClaimsNames.add("aud");
079                reservedClaimsNames.add("exp");
080                reservedClaimsNames.add("nbf");
081                reservedClaimsNames.add("iat");
082                reservedClaimsNames.add("jti");
083        }
084
085
086        /**
087         * Gets the names of the reserved JWT bearer assertion claims.
088         *
089         * @return The names of the reserved JWT bearer assertion claims
090         *         (read-only set).
091         */
092        public static Set<String> getReservedClaimsNames() {
093
094                return Collections.unmodifiableSet(reservedClaimsNames);
095        }
096
097
098        /**
099         * The time before which this token must not be accepted for
100         * processing (optional). The serialised value is number of seconds
101         * from 1970-01-01T0:0:0Z as measured in UTC until the desired
102         * date/time.
103         */
104        private final Date nbf;
105
106
107        /**
108         * Other optional custom claims.
109         */
110        private final Map<String,Object> other;
111
112
113        /**
114         * Creates a new JWT bearer assertion details (claims set) instance.
115         * The expiration time (exp) is set to five minutes from the current
116         * system time. Generates a default identifier (jti) for the JWT. The
117         * issued-at (iat) and not-before (nbf) claims are not set.
118         *
119         * @param iss The issuer identifier. Must not be {@code null}.
120         * @param sub The subject. Must not be {@code null}.
121         * @param aud The audience, typically the authorisation server issuer
122         *            URI. Must not be {@code null}.
123         */
124        public JWTAssertionDetails(final Issuer iss,
125                                   final Subject sub,
126                                   final Audience aud) {
127
128                this(iss, sub, aud.toSingleAudienceList(), new Date(new Date().getTime() + 5*60*1000L), null, null, new JWTID(), null);
129        }
130
131
132        /**
133         * Creates a new JWT bearer assertion details (claims set) instance.
134         *
135         * @param iss   The issuer identifier. Must not be {@code null}.
136         * @param sub   The subject. Must not be {@code null}.
137         * @param aud   The audience, typically a singleton list with the
138         *              authorisation server issuer URI. Must not be
139         *              {@code null}.
140         * @param exp   The expiration time. Must not be {@code null}.
141         * @param nbf   The time before which the token must not be accepted
142         *              for processing, {@code null} if not specified.
143         * @param iat   The time at which the token was issued, {@code null} if
144         *              not specified.
145         * @param jti   Unique identifier for the JWT, {@code null} if not
146         *              specified.
147         * @param other Other custom claims to include, {@code null} if none.
148         */
149        public JWTAssertionDetails(final Issuer iss,
150                                   final Subject sub,
151                                   final List<Audience> aud,
152                                   final Date exp,
153                                   final Date nbf,
154                                   final Date iat,
155                                   final JWTID jti,
156                                   final Map<String,Object> other) {
157
158                super(iss, sub, aud, iat, exp, jti);
159                this.nbf = nbf;
160                this.other = other;
161        }
162        
163        
164        /**
165         * Returns the optional not-before time. Corresponds to the {@code nbf}
166         * claim.
167         *
168         * @return The not-before time, {@code null} if not specified.
169         */
170        public Date getNotBeforeTime() {
171        
172                return nbf;
173        }
174
175
176        /**
177         * Returns the optional assertion identifier, as a JWT ID. Corresponds
178         * to the {@code jti} claim.
179         *
180         * @see #getID()
181         *
182         * @return The optional JWT ID, {@code null} if not specified.
183         */
184        public JWTID getJWTID() {
185
186                Identifier id = getID();
187                return id != null ? new JWTID(id.getValue()) : null;
188        }
189
190
191        /**
192         * Returns the custom claims.
193         *
194         * @return The custom claims, {@code null} if not specified.
195         */
196        public Map<String,Object> getCustomClaims() {
197
198                return other;
199        }
200        
201        
202        /**
203         * Returns a JSON object representation of this JWT bearer assertion
204         * details.
205         *
206         * @return The JSON object.
207         */
208        public JSONObject toJSONObject() {
209        
210                JSONObject o = new JSONObject();
211                
212                o.put("iss", getIssuer().getValue());
213                o.put("sub", getSubject().getValue());
214                List<String> audStringList = Audience.toStringList(getAudience());
215                if (audStringList != null) {
216                        if (audStringList.size() == 1) {
217                                o.put("aud", audStringList.get(0));
218                        } else {
219                                o.put("aud", audStringList);
220                        }
221                }
222                o.put("exp", DateUtils.toSecondsSinceEpoch(getExpirationTime()));
223
224                if (nbf != null)
225                        o.put("nbf", DateUtils.toSecondsSinceEpoch(nbf));
226                
227                if (getIssueTime() != null)
228                        o.put("iat", DateUtils.toSecondsSinceEpoch(getIssueTime()));
229                
230                if (getID() != null)
231                        o.put("jti", getID().getValue());
232
233                if (other != null) {
234                        o.putAll(other);
235                }
236
237                return o;
238        }
239
240
241        /**
242         * Returns a JSON Web Token (JWT) claims set representation of this
243         * JWT bearer assertion details.
244         *
245         * @return The JWT claims set.
246         */
247        public JWTClaimsSet toJWTClaimsSet() {
248
249                JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
250                        .issuer(getIssuer().getValue())
251                        .subject(getSubject().getValue())
252                        .audience(Audience.toStringList(getAudience()))
253                        .expirationTime(getExpirationTime())
254                        .notBeforeTime(nbf) // optional
255                        .issueTime(getIssueTime()) // optional
256                        .jwtID(getID() != null ? getJWTID().getValue() : null); // optional
257
258                // Append custom claims if any
259                if (other != null) {
260                        for (Map.Entry<String,?> entry: other.entrySet()) {
261                                builder = builder.claim(entry.getKey(), entry.getValue());
262                        }
263                }
264
265                return builder.build();
266        }
267        
268        
269        /**
270         * Parses a JWT bearer assertion details (claims set) instance from the
271         * specified JSON object.
272         *
273         * @param jsonObject The JSON object. Must not be {@code null}.
274         *
275         * @return The JWT bearer assertion details.
276         *
277         * @throws ParseException If the JSON object couldn't be parsed to a 
278         *                        JWT bearer assertion details instance.
279         */
280        public static JWTAssertionDetails parse(final JSONObject jsonObject)
281                throws ParseException {
282                
283                // Parse required claims
284                Issuer iss = new Issuer(JSONObjectUtils.getNonBlankString(jsonObject, "iss"));
285                Subject sub = new Subject(JSONObjectUtils.getNonBlankString(jsonObject, "sub"));
286
287                List<Audience> aud;
288
289                if (jsonObject.get("aud") instanceof String) {
290                        aud = new Audience(JSONObjectUtils.getNonBlankString(jsonObject, "aud")).toSingleAudienceList();
291                } else {
292                        aud = Audience.create(JSONObjectUtils.getStringList(jsonObject, "aud"));
293                }
294
295                Date exp = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "exp"));
296
297
298                // Parse optional claims
299
300                Date nbf = null;
301
302                if (jsonObject.containsKey("nbf"))
303                        nbf = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "nbf"));
304
305                Date iat = null;
306
307                if (jsonObject.containsKey("iat"))
308                        iat = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "iat"));
309
310                JWTID jti = null;
311
312                if (jsonObject.containsKey("jti"))
313                        jti = new JWTID(JSONObjectUtils.getNonBlankString(jsonObject, "jti"));
314
315                // Parse custom claims
316                Map<String,Object> other = null;
317
318                Set<String> customClaimNames = jsonObject.keySet();
319                if (customClaimNames.removeAll(reservedClaimsNames)) {
320                        other = new LinkedHashMap<>();
321                        for (String claim: customClaimNames) {
322                                other.put(claim, jsonObject.get(claim));
323                        }
324                }
325
326                return new JWTAssertionDetails(iss, sub, aud, exp, nbf, iat, jti, other);
327        }
328
329
330        /**
331         * Parses a JWT bearer assertion details instance from the specified
332         * JWT claims set.
333         *
334         * @param jwtClaimsSet The JWT claims set. Must not be {@code null}.
335         *
336         * @return The JWT bearer assertion details.
337         *
338         * @throws ParseException If the JWT claims set couldn't be parsed to a 
339         *                        JWT bearer assertion details instance.
340         */
341        public static JWTAssertionDetails parse(final JWTClaimsSet jwtClaimsSet)
342                throws ParseException {
343                
344                return parse(JSONObjectUtils.toJSONObject(jwtClaimsSet));
345        }
346}