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.KeySourceException;
030import com.nimbusds.jose.RemoteKeySourceException;
031import com.nimbusds.jose.jwk.JWK;
032import com.nimbusds.jose.jwk.JWKMatcher;
033import com.nimbusds.jose.jwk.JWKSelector;
034import com.nimbusds.jose.jwk.JWKSet;
035import com.nimbusds.jose.proc.SecurityContext;
036import com.nimbusds.jose.util.DefaultResourceRetriever;
037import com.nimbusds.jose.util.Resource;
038import com.nimbusds.jose.util.ResourceRetriever;
039
040
041/**
042 * Remote JSON Web Key (JWK) source specified by a JWK set URL. The retrieved
043 * JWK set is cached to minimise network calls. The cache is updated whenever
044 * the key selector tries to get a key with an unknown ID or the cache expires.
045 *
046 * <p>If no {@link ResourceRetriever} is specified when creating a remote JWK
047 * set source the {@link DefaultResourceRetriever default one} will be used,
048 * with the following HTTP timeouts and limits:
049 *
050 * <ul>
051 *     <li>HTTP connect timeout, in milliseconds: Determined by the
052 *         {@link #DEFAULT_HTTP_CONNECT_TIMEOUT} constant which can be
053 *         overridden by setting the
054 *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout}
055 *         Java system property.
056 *     <li>HTTP read timeout, in milliseconds: Determined by the
057 *         {@link #DEFAULT_HTTP_READ_TIMEOUT} constant which can be
058 *         overridden by setting the
059 *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout}
060 *         Java system property.
061 *     <li>HTTP entity size limit: Determined by the
062 *         {@link #DEFAULT_HTTP_SIZE_LIMIT} constant which can be
063 *         overridden by setting the
064 *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit}
065 *         Java system property.
066 * </ul>
067 *
068 * <p>A failover JWK source can be configured in case the JWK set URL becomes
069 * unavailable (HTTP 404) or times out. The failover JWK source can be another
070 * URL or some other object.
071 *
072 * @author Vladimir Dzhuvinov
073 * @author Andreas Huber
074 * @version 2022-01-30
075 * @deprecated Construct a {@linkplain JWKSource} using {@linkplain JWKSourceBuilder}.
076 */
077@ThreadSafe
078@Deprecated
079public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> {
080
081
082        /**
083         * The default HTTP connect timeout for JWK set retrieval, in
084         * milliseconds. Set to 500 milliseconds.
085         */
086        public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 500;
087
088
089        /**
090         * The default HTTP read timeout for JWK set retrieval, in
091         * milliseconds. Set to 500 milliseconds.
092         */
093        public static final int DEFAULT_HTTP_READ_TIMEOUT = 500;
094
095
096        /**
097         * The default HTTP entity size limit for JWK set retrieval, in bytes.
098         * Set to 50 KBytes.
099         */
100        public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024;
101        
102        
103        /**
104         * Resolves the default HTTP connect timeout for JWK set retrieval, in
105         * milliseconds.
106         *
107         * @return The {@link #DEFAULT_HTTP_CONNECT_TIMEOUT static constant},
108         *         overridden by setting the
109         *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout}
110         *         Java system property.
111         */
112        public static int resolveDefaultHTTPConnectTimeout() {
113                return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpConnectTimeout", DEFAULT_HTTP_CONNECT_TIMEOUT);
114        }
115        
116        
117        /**
118         * Resolves the default HTTP read timeout for JWK set retrieval, in
119         * milliseconds.
120         *
121         * @return The {@link #DEFAULT_HTTP_READ_TIMEOUT static constant},
122         *         overridden by setting the
123         *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout}
124         *         Java system property.
125         */
126        public static int resolveDefaultHTTPReadTimeout() {
127                return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpReadTimeout", DEFAULT_HTTP_READ_TIMEOUT);
128        }
129        
130        
131        /**
132         * Resolves default HTTP entity size limit for JWK set retrieval, in
133         * bytes.
134         *
135         * @return The {@link #DEFAULT_HTTP_SIZE_LIMIT static constant},
136         *         overridden by setting the
137         *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit}
138         *         Java system property.
139         */
140        public static int resolveDefaultHTTPSizeLimit() {
141                return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpSizeLimit", DEFAULT_HTTP_SIZE_LIMIT);
142        }
143        
144        
145        private static int resolveDefault(final String sysPropertyName, final int defaultValue) {
146                
147                String value = System.getProperty(sysPropertyName);
148                
149                if (value == null) {
150                        return defaultValue;
151                }
152                
153                try {
154                        return Integer.parseInt(value);
155                } catch (NumberFormatException e) {
156                        // Illegal value
157                        return defaultValue;
158                }
159        }
160
161
162        /**
163         * The JWK set URL.
164         */
165        private final URL jwkSetURL;
166        
167        
168        /**
169         * Optional failover JWK source.
170         */
171        private final JWKSource<C> failoverJWKSource;
172        
173
174        /**
175         * The JWK set cache.
176         */
177        private final JWKSetCache jwkSetCache;
178
179
180        /**
181         * The JWK set retriever.
182         */
183        private final ResourceRetriever jwkSetRetriever;
184
185
186        /**
187         * Creates a new remote JWK set using the
188         * {@link DefaultResourceRetriever default HTTP resource retriever}
189         * with the default HTTP timeouts and entity size limit.
190         *
191         * @param jwkSetURL The JWK set URL. Must not be {@code null}.
192         */
193        public RemoteJWKSet(final URL jwkSetURL) {
194                this(jwkSetURL, (JWKSource<C>) null);
195        }
196
197
198        /**
199         * Creates a new remote JWK set using the
200         * {@link DefaultResourceRetriever default HTTP resource retriever}
201         * with the default HTTP timeouts and entity size limit.
202         *
203         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
204         * @param failoverJWKSource Optional failover JWK source in case
205         *                          retrieval from the JWK set URL fails,
206         *                          {@code null} if no failover is specified.
207         */
208        public RemoteJWKSet(final URL jwkSetURL, final JWKSource<C> failoverJWKSource) {
209                this(jwkSetURL, failoverJWKSource, null, null);
210        }
211
212
213        /**
214         * Creates a new remote JWK set.
215         *
216         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
217         * @param resourceRetriever The HTTP resource retriever to use,
218         *                          {@code null} to use the
219         *                          {@link DefaultResourceRetriever default
220         *                          one} with the default HTTP timeouts and
221         *                          entity size limit.
222         */
223        public RemoteJWKSet(final URL jwkSetURL,
224                            final ResourceRetriever resourceRetriever) {
225                
226                this(jwkSetURL, resourceRetriever, null);
227        }
228
229
230        /**
231         * Creates a new remote JWK set.
232         *
233         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
234         * @param resourceRetriever The HTTP resource retriever to use,
235         *                          {@code null} to use the
236         *                          {@link DefaultResourceRetriever default
237         *                          one} with the default HTTP timeouts and
238         *                          entity size limit.
239         * @param jwkSetCache       The JWK set cache to use, {@code null} to
240         *                          use the {@link DefaultJWKSetCache default
241         *                          one}.
242         */
243        public RemoteJWKSet(final URL jwkSetURL,
244                            final ResourceRetriever resourceRetriever,
245                            final JWKSetCache jwkSetCache) {
246                
247                this(jwkSetURL, null, resourceRetriever, jwkSetCache);
248        }
249
250
251        /**
252         * Creates a new remote JWK set.
253         *
254         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
255         * @param failoverJWKSource Optional failover JWK source in case
256         *                          retrieval from the JWK set URL fails,
257         *                          {@code null} if no failover is specified.
258         * @param resourceRetriever The HTTP resource retriever to use,
259         *                          {@code null} to use the
260         *                          {@link DefaultResourceRetriever default
261         *                          one} with the default HTTP timeouts and
262         *                          entity size limit.
263         * @param jwkSetCache       The JWK set cache to use, {@code null} to
264         *                          use the {@link DefaultJWKSetCache default
265         *                          one}.
266         */
267        public RemoteJWKSet(final URL jwkSetURL,
268                            final JWKSource<C> failoverJWKSource,
269                            final ResourceRetriever resourceRetriever,
270                            final JWKSetCache jwkSetCache) {
271                
272                if (jwkSetURL == null) {
273                        throw new IllegalArgumentException("The JWK set URL must not be null");
274                }
275                this.jwkSetURL = jwkSetURL;
276                
277                this.failoverJWKSource = failoverJWKSource;
278
279                if (resourceRetriever != null) {
280                        jwkSetRetriever = resourceRetriever;
281                } else {
282                        jwkSetRetriever = new DefaultResourceRetriever(
283                                resolveDefaultHTTPConnectTimeout(),
284                                resolveDefaultHTTPReadTimeout(),
285                                resolveDefaultHTTPSizeLimit());
286                }
287                
288                if (jwkSetCache != null) {
289                        this.jwkSetCache = jwkSetCache;
290                } else {
291                        this.jwkSetCache = new DefaultJWKSetCache();
292                }
293        }
294
295
296        /**
297         * Updates the cached JWK set from the configured URL.
298         *
299         * @return The updated JWK set.
300         *
301         * @throws RemoteKeySourceException If JWK retrieval failed.
302         */
303        private JWKSet updateJWKSetFromURL()
304                throws RemoteKeySourceException {
305                Resource res;
306                try {
307                        res = jwkSetRetriever.retrieveResource(jwkSetURL);
308                } catch (IOException e) {
309                        throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e);
310                }
311                JWKSet jwkSet;
312                try {
313                        jwkSet = JWKSet.parse(res.getContent());
314                } catch (java.text.ParseException e) {
315                        throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e);
316                }
317                jwkSetCache.put(jwkSet);
318                return jwkSet;
319        }
320
321
322        /**
323         * Returns the JWK set URL.
324         *
325         * @return The JWK set URL.
326         */
327        public URL getJWKSetURL() {
328                
329                return jwkSetURL;
330        }
331        
332        
333        /**
334         * Returns the optional failover JWK source.
335         *
336         * @return The failover JWK source, {@code null} if not specified.
337         */
338        public JWKSource<C> getFailoverJWKSource() {
339                
340                return failoverJWKSource;
341        }
342        
343        
344        /**
345         * Returns the HTTP resource retriever.
346         *
347         * @return The HTTP resource retriever.
348         */
349        public ResourceRetriever getResourceRetriever() {
350
351                return jwkSetRetriever;
352        }
353        
354        
355        /**
356         * Returns the configured JWK set cache.
357         *
358         * @return The JWK set cache.
359         */
360        public JWKSetCache getJWKSetCache() {
361                
362                return jwkSetCache;
363        }
364        
365        
366        /**
367         * Returns the cached JWK set.
368         *
369         * @return The cached JWK set, {@code null} if none or expired.
370         */
371        public JWKSet getCachedJWKSet() {
372                
373                return jwkSetCache.get();
374        }
375
376
377        /**
378         * Returns the first specified key ID (kid) for a JWK matcher.
379         *
380         * @param jwkMatcher The JWK matcher. Must not be {@code null}.
381         *
382         * @return The first key ID, {@code null} if none.
383         */
384        protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) {
385
386                Set<String> keyIDs = jwkMatcher.getKeyIDs();
387
388                if (keyIDs == null || keyIDs.isEmpty()) {
389                        return null;
390                }
391
392                for (String id: keyIDs) {
393                        if (id != null) {
394                                return id;
395                        }
396                }
397                return null; // No kid in matcher
398        }
399        
400        
401        /**
402         * Fails over to the configuration optional JWK source.
403         */
404        private List<JWK> failover(final Exception exception, final JWKSelector jwkSelector, final C context)
405                throws RemoteKeySourceException{
406                
407                if (getFailoverJWKSource() == null) {
408                        return null;
409                }
410                
411                try {
412                        return getFailoverJWKSource().get(jwkSelector, context);
413                } catch (KeySourceException kse) {
414                        throw new RemoteKeySourceException(
415                                exception.getMessage() +
416                                "; Failover JWK source retrieval failed with: " + kse.getMessage(),
417                                kse
418                        );
419                }
420        }
421        
422        
423        @Override
424        public List<JWK> get(final JWKSelector jwkSelector, final C context)
425                throws RemoteKeySourceException {
426
427                // Check the cache first
428                JWKSet jwkSet = jwkSetCache.get();
429                
430                if (jwkSetCache.requiresRefresh() || jwkSet == null) {
431                        // JWK set update required
432                        try {
433                                // Prevent multiple cache updates in case of concurrent requests
434                                // (with double-checked locking, i.e. locking on update required only)
435                                synchronized (this) {
436                                        jwkSet = jwkSetCache.get();
437                                        if (jwkSetCache.requiresRefresh() || jwkSet == null) {
438                                                // Retrieve JWK set from URL
439                                                jwkSet = updateJWKSetFromURL();
440                                        }
441                                }
442                        } catch (Exception e) {
443                                
444                                List<JWK> failoverMatches = failover(e, jwkSelector, context);
445                                if (failoverMatches != null) {
446                                        return failoverMatches; // Failover success
447                                }
448                                
449                                if (jwkSet == null) {
450                                        // Rethrow the received exception if expired
451                                        throw e;
452                                }
453                                
454                                // Continue with cached version
455                        }
456                }
457
458                // Run the selector on the JWK set
459                List<JWK> matches = jwkSelector.select(jwkSet);
460
461                if (! matches.isEmpty()) {
462                        // Success
463                        return matches;
464                }
465
466                // Refresh the JWK set if the sought key ID is not in the cached JWK set
467
468                // Looking for JWK with specific ID?
469                String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher());
470                if (soughtKeyID == null) {
471                        // No key ID specified, return no matches
472                        return Collections.emptyList();
473                }
474
475                if (jwkSet.getKeyByKeyId(soughtKeyID) != null) {
476                        // The key ID exists in the cached JWK set, matching
477                        // failed for some other reason, return no matches
478                        return Collections.emptyList();
479                }
480                
481                try {
482                        // If the jwkSet in the cache is not the same instance that was
483                        // in the cache at the beginning of this method, then we know
484                        // the cache was updated
485                        synchronized (this) {
486                                if (jwkSet == jwkSetCache.get()) {
487                                        // Make new HTTP GET to the JWK set URL
488                                        jwkSet = updateJWKSetFromURL();
489                                } else {
490                                        // Cache was updated recently, the cached value is up-to-date
491                                        jwkSet = jwkSetCache.get();
492                                }
493                        }
494                } catch (KeySourceException e) {
495                        
496                        List<JWK> failoverMatches = failover(e, jwkSelector, context);
497                        if (failoverMatches != null) {
498                                return failoverMatches; // Failover success
499                        }
500                        
501                        throw e;
502                }
503                
504                
505                if (jwkSet == null) {
506                        // Retrieval has failed
507                        return Collections.emptyList();
508                }
509
510                // Repeat select, return final result (success or no matches)
511                return jwkSelector.select(jwkSet);
512        }
513}