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