001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2016, Connect2id Ltd.
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.jose.jwk.source;
019
020
021import java.io.IOException;
022import java.net.URL;
023import java.util.Collections;
024import java.util.List;
025import java.util.Set;
026
027import net.jcip.annotations.ThreadSafe;
028
029import com.nimbusds.jose.RemoteKeySourceException;
030import com.nimbusds.jose.jwk.JWK;
031import com.nimbusds.jose.jwk.JWKMatcher;
032import com.nimbusds.jose.jwk.JWKSelector;
033import com.nimbusds.jose.jwk.JWKSet;
034import com.nimbusds.jose.proc.SecurityContext;
035import com.nimbusds.jose.util.DefaultResourceRetriever;
036import com.nimbusds.jose.util.Resource;
037import com.nimbusds.jose.util.ResourceRetriever;
038
039
040/**
041 * Remote JSON Web Key (JWK) source specified by a JWK set URL. The retrieved
042 * JWK set is cached to minimise network calls. The cache is updated whenever
043 * the key selector tries to get a key with an unknown ID.
044 *
045 * <p>If no {@link ResourceRetriever} is specified when creating a remote JWK
046 * set source the {@link DefaultResourceRetriever default one} will be used,
047 * with the following HTTP timeouts and limits:
048 *
049 * <ul>
050 *     <li>HTTP connect timeout, in milliseconds: Determined by the
051 *         {@link #DEFAULT_HTTP_CONNECT_TIMEOUT} constant which can be
052 *         overridden by setting the
053 *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout}
054 *         Java system property.
055 *     <li>HTTP read timeout, in milliseconds: Determined by the
056 *         {@link #DEFAULT_HTTP_READ_TIMEOUT} constant which can be
057 *         overridden by setting the
058 *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout}
059 *         Java system property.
060 *     <li>HTTP entity size limit: Determined by the
061 *         {@link #DEFAULT_HTTP_SIZE_LIMIT} constant which can be
062 *         overridden by setting the
063 *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit}
064 *         Java system property.
065 * </ul>
066 *
067 * @author Vladimir Dzhuvinov
068 * @author Andreas Huber
069 * @version 2022-01-24
070 */
071@ThreadSafe
072public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> {
073
074
075        /**
076         * The default HTTP connect timeout for JWK set retrieval, in
077         * milliseconds. Set to 500 milliseconds.
078         */
079        public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 500;
080
081
082        /**
083         * The default HTTP read timeout for JWK set retrieval, in
084         * milliseconds. Set to 500 milliseconds.
085         */
086        public static final int DEFAULT_HTTP_READ_TIMEOUT = 500;
087
088
089        /**
090         * The default HTTP entity size limit for JWK set retrieval, in bytes.
091         * Set to 50 KBytes.
092         */
093        public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024;
094        
095        
096        /**
097         * Resolves the default HTTP connect timeout for JWK set retrieval, in
098         * milliseconds.
099         *
100         * @return The {@link #DEFAULT_HTTP_CONNECT_TIMEOUT static constant},
101         *         overridden by setting the
102         *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout}
103         *         Java system property.
104         */
105        public static int resolveDefaultHTTPConnectTimeout() {
106                return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpConnectTimeout", DEFAULT_HTTP_CONNECT_TIMEOUT);
107        }
108        
109        
110        /**
111         * Resolves the default HTTP read timeout for JWK set retrieval, in
112         * milliseconds.
113         *
114         * @return The {@link #DEFAULT_HTTP_READ_TIMEOUT static constant},
115         *         overridden by setting the
116         *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout}
117         *         Java system property.
118         */
119        public static int resolveDefaultHTTPReadTimeout() {
120                return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpReadTimeout", DEFAULT_HTTP_READ_TIMEOUT);
121        }
122        
123        
124        /**
125         * Resolves default HTTP entity size limit for JWK set retrieval, in
126         * bytes.
127         *
128         * @return The {@link #DEFAULT_HTTP_SIZE_LIMIT static constant},
129         *         overridden by setting the
130         *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit}
131         *         Java system property.
132         */
133        public static int resolveDefaultHTTPSizeLimit() {
134                return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpSizeLimit", DEFAULT_HTTP_SIZE_LIMIT);
135        }
136        
137        
138        private static int resolveDefault(final String sysPropertyName, final int defaultValue) {
139                
140                String value = System.getProperty(sysPropertyName);
141                
142                if (value == null) {
143                        return defaultValue;
144                }
145                
146                try {
147                        return Integer.parseInt(value);
148                } catch (NumberFormatException e) {
149                        // Illegal value
150                        return defaultValue;
151                }
152        }
153
154
155        /**
156         * The JWK set URL.
157         */
158        private final URL jwkSetURL;
159        
160
161        /**
162         * The JWK set cache.
163         */
164        private final JWKSetCache jwkSetCache;
165
166
167        /**
168         * The JWK set retriever.
169         */
170        private final ResourceRetriever jwkSetRetriever;
171
172
173        /**
174         * Creates a new remote JWK set using the
175         * {@link DefaultResourceRetriever default HTTP resource retriever}
176         * with the default HTTP timeouts and entity size limit.
177         *
178         * @param jwkSetURL The JWK set URL. Must not be {@code null}.
179         */
180        public RemoteJWKSet(final URL jwkSetURL) {
181                this(jwkSetURL, null);
182        }
183
184
185        /**
186         * Creates a new remote JWK set.
187         *
188         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
189         * @param resourceRetriever The HTTP resource retriever to use,
190         *                          {@code null} to use the
191         *                          {@link DefaultResourceRetriever default
192         *                          one} with the default HTTP timeouts and
193         *                          entity size limit.
194         */
195        public RemoteJWKSet(final URL jwkSetURL,
196                            final ResourceRetriever resourceRetriever) {
197                
198                this(jwkSetURL, resourceRetriever, null);
199        }
200
201
202        /**
203         * Creates a new remote JWK set.
204         *
205         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
206         * @param resourceRetriever The HTTP resource retriever to use,
207         *                          {@code null} to use the
208         *                          {@link DefaultResourceRetriever default
209         *                          one} with the default HTTP timeouts and
210         *                          entity size limit.
211         * @param jwkSetCache       The JWK set cache to use, {@code null} to
212         *                          use the {@link DefaultJWKSetCache default
213         *                          one}.
214         */
215        public RemoteJWKSet(final URL jwkSetURL,
216                            final ResourceRetriever resourceRetriever,
217                            final JWKSetCache jwkSetCache) {
218                
219                if (jwkSetURL == null) {
220                        throw new IllegalArgumentException("The JWK set URL must not be null");
221                }
222                this.jwkSetURL = jwkSetURL;
223
224                if (resourceRetriever != null) {
225                        jwkSetRetriever = resourceRetriever;
226                } else {
227                        jwkSetRetriever = new DefaultResourceRetriever(
228                                resolveDefaultHTTPConnectTimeout(),
229                                resolveDefaultHTTPReadTimeout(),
230                                resolveDefaultHTTPSizeLimit());
231                }
232                
233                if (jwkSetCache != null) {
234                        this.jwkSetCache = jwkSetCache;
235                } else {
236                        this.jwkSetCache = new DefaultJWKSetCache();
237                }
238        }
239
240
241        /**
242         * Updates the cached JWK set from the configured URL.
243         *
244         * @return The updated JWK set.
245         *
246         * @throws RemoteKeySourceException If JWK retrieval failed.
247         */
248        private JWKSet updateJWKSetFromURL()
249                throws RemoteKeySourceException {
250                Resource res;
251                try {
252                        res = jwkSetRetriever.retrieveResource(jwkSetURL);
253                } catch (IOException e) {
254                        throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e);
255                }
256                JWKSet jwkSet;
257                try {
258                        jwkSet = JWKSet.parse(res.getContent());
259                } catch (java.text.ParseException e) {
260                        throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e);
261                }
262                jwkSetCache.put(jwkSet);
263                return jwkSet;
264        }
265
266
267        /**
268         * Returns the JWK set URL.
269         *
270         * @return The JWK set URL.
271         */
272        public URL getJWKSetURL() {
273                
274                return jwkSetURL;
275        }
276
277
278        /**
279         * Returns the HTTP resource retriever.
280         *
281         * @return The HTTP resource retriever.
282         */
283        public ResourceRetriever getResourceRetriever() {
284
285                return jwkSetRetriever;
286        }
287        
288        
289        /**
290         * Returns the configured JWK set cache.
291         *
292         * @return The JWK set cache.
293         */
294        public JWKSetCache getJWKSetCache() {
295                
296                return jwkSetCache;
297        }
298        
299        
300        /**
301         * Returns the cached JWK set.
302         *
303         * @return The cached JWK set, {@code null} if none or expired.
304         */
305        public JWKSet getCachedJWKSet() {
306                
307                return jwkSetCache.get();
308        }
309
310
311        /**
312         * Returns the first specified key ID (kid) for a JWK matcher.
313         *
314         * @param jwkMatcher The JWK matcher. Must not be {@code null}.
315         *
316         * @return The first key ID, {@code null} if none.
317         */
318        protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) {
319
320                Set<String> keyIDs = jwkMatcher.getKeyIDs();
321
322                if (keyIDs == null || keyIDs.isEmpty()) {
323                        return null;
324                }
325
326                for (String id: keyIDs) {
327                        if (id != null) {
328                                return id;
329                        }
330                }
331                return null; // No kid in matcher
332        }
333
334
335        /**
336         * {@inheritDoc} The security context is ignored.
337         */
338        @Override
339        public List<JWK> get(final JWKSelector jwkSelector, final C context)
340                throws RemoteKeySourceException {
341
342                // Get the JWK set, may necessitate a cache update.
343                JWKSet jwkSet = jwkSetCache.get();
344                if (jwkSetCache.requiresRefresh() || jwkSet == null) {
345                        try {
346                                // Prevent multiple cache updates in case of concurrent requests
347                                // (with double-checked locking / locking on update required only)
348                                synchronized (this) {
349                                        jwkSet = jwkSetCache.get();
350                                        if (jwkSetCache.requiresRefresh() || jwkSet == null) {
351                                                // Retrieve jwkSet by calling JWK set URL
352                                                jwkSet = updateJWKSetFromURL();
353                                        }
354                                }
355                        } catch (Exception ex) {
356                                if (jwkSet == null) {
357                                        // Rethrow the received exception if expired
358                                        throw  ex;
359                                }
360                        }
361                }
362
363                // Run the selector on the JWK set
364                List<JWK> matches = jwkSelector.select(jwkSet);
365
366                if (! matches.isEmpty()) {
367                        // Success
368                        return matches;
369                }
370
371                // Refresh the JWK set if the sought key ID is not in the cached JWK set
372
373                // Looking for JWK with specific ID?
374                String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher());
375                if (soughtKeyID == null) {
376                        // No key ID specified, return no matches
377                        return Collections.emptyList();
378                }
379
380                if (jwkSet.getKeyByKeyId(soughtKeyID) != null) {
381                        // The key ID exists in the cached JWK set, matching
382                        // failed for some other reason, return no matches
383                        return Collections.emptyList();
384                }
385                
386                // If the jwkSet in the cache is not the same instance that was
387                // in the cache at the beginning of this method, then we know
388                // the cache was updated
389                synchronized (this) {
390                        if (jwkSet == jwkSetCache.get()) {
391                                // Make new HTTP GET to the JWK set URL
392                                jwkSet = updateJWKSetFromURL();
393                        } else {
394                                // Cache was updated recently, the cached value is up-to-date
395                                jwkSet = jwkSetCache.get();
396                        }
397                }
398                
399                if (jwkSet == null) {
400                        // Retrieval has failed
401                        return Collections.emptyList();
402                }
403
404                // Repeat select, return final result (success or no matches)
405                return jwkSelector.select(jwkSet);
406        }
407}