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 com.nimbusds.jose.KeySourceException;
022import com.nimbusds.jose.RemoteKeySourceException;
023import com.nimbusds.jose.jwk.JWK;
024import com.nimbusds.jose.jwk.JWKMatcher;
025import com.nimbusds.jose.jwk.JWKSelector;
026import com.nimbusds.jose.jwk.JWKSet;
027import com.nimbusds.jose.proc.SecurityContext;
028import com.nimbusds.jose.util.DefaultResourceRetriever;
029import com.nimbusds.jose.util.Resource;
030import com.nimbusds.jose.util.ResourceRetriever;
031import net.jcip.annotations.ThreadSafe;
032
033import java.io.IOException;
034import java.net.URL;
035import java.util.Collections;
036import java.util.List;
037import java.util.Objects;
038import java.util.Set;
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 2024-04-20
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                this.jwkSetURL = Objects.requireNonNull(jwkSetURL);
273                
274                this.failoverJWKSource = failoverJWKSource;
275
276                if (resourceRetriever != null) {
277                        jwkSetRetriever = resourceRetriever;
278                } else {
279                        jwkSetRetriever = new DefaultResourceRetriever(
280                                resolveDefaultHTTPConnectTimeout(),
281                                resolveDefaultHTTPReadTimeout(),
282                                resolveDefaultHTTPSizeLimit());
283                }
284                
285                if (jwkSetCache != null) {
286                        this.jwkSetCache = jwkSetCache;
287                } else {
288                        this.jwkSetCache = new DefaultJWKSetCache();
289                }
290        }
291
292
293        /**
294         * Updates the cached JWK set from the configured URL.
295         *
296         * @return The updated JWK set.
297         *
298         * @throws RemoteKeySourceException If JWK retrieval failed.
299         */
300        private JWKSet updateJWKSetFromURL()
301                throws RemoteKeySourceException {
302                Resource res;
303                try {
304                        res = jwkSetRetriever.retrieveResource(jwkSetURL);
305                } catch (IOException e) {
306                        throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e);
307                }
308                JWKSet jwkSet;
309                try {
310                        jwkSet = JWKSet.parse(res.getContent());
311                } catch (java.text.ParseException e) {
312                        throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e);
313                }
314                jwkSetCache.put(jwkSet);
315                return jwkSet;
316        }
317
318
319        /**
320         * Returns the JWK set URL.
321         *
322         * @return The JWK set URL.
323         */
324        public URL getJWKSetURL() {
325                
326                return jwkSetURL;
327        }
328        
329        
330        /**
331         * Returns the optional failover JWK source.
332         *
333         * @return The failover JWK source, {@code null} if not specified.
334         */
335        public JWKSource<C> getFailoverJWKSource() {
336                
337                return failoverJWKSource;
338        }
339        
340        
341        /**
342         * Returns the HTTP resource retriever.
343         *
344         * @return The HTTP resource retriever.
345         */
346        public ResourceRetriever getResourceRetriever() {
347
348                return jwkSetRetriever;
349        }
350        
351        
352        /**
353         * Returns the configured JWK set cache.
354         *
355         * @return The JWK set cache.
356         */
357        public JWKSetCache getJWKSetCache() {
358                
359                return jwkSetCache;
360        }
361        
362        
363        /**
364         * Returns the cached JWK set.
365         *
366         * @return The cached JWK set, {@code null} if none or expired.
367         */
368        public JWKSet getCachedJWKSet() {
369                
370                return jwkSetCache.get();
371        }
372
373
374        /**
375         * Returns the first specified key ID (kid) for a JWK matcher.
376         *
377         * @param jwkMatcher The JWK matcher. Must not be {@code null}.
378         *
379         * @return The first key ID, {@code null} if none.
380         */
381        protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) {
382
383                Set<String> keyIDs = jwkMatcher.getKeyIDs();
384
385                if (keyIDs == null || keyIDs.isEmpty()) {
386                        return null;
387                }
388
389                for (String id: keyIDs) {
390                        if (id != null) {
391                                return id;
392                        }
393                }
394                return null; // No kid in matcher
395        }
396        
397        
398        /**
399         * Fails over to the configuration optional JWK source.
400         */
401        private List<JWK> failover(final Exception exception, final JWKSelector jwkSelector, final C context)
402                throws RemoteKeySourceException{
403                
404                if (getFailoverJWKSource() == null) {
405                        return null;
406                }
407                
408                try {
409                        return getFailoverJWKSource().get(jwkSelector, context);
410                } catch (KeySourceException kse) {
411                        throw new RemoteKeySourceException(
412                                exception.getMessage() +
413                                "; Failover JWK source retrieval failed with: " + kse.getMessage(),
414                                kse
415                        );
416                }
417        }
418        
419        
420        @Override
421        public List<JWK> get(final JWKSelector jwkSelector, final C context)
422                throws RemoteKeySourceException {
423
424                // Check the cache first
425                JWKSet jwkSet = jwkSetCache.get();
426                
427                if (jwkSetCache.requiresRefresh() || jwkSet == null) {
428                        // JWK set update required
429                        try {
430                                // Prevent multiple cache updates in case of concurrent requests
431                                // (with double-checked locking, i.e. locking on update required only)
432                                synchronized (this) {
433                                        jwkSet = jwkSetCache.get();
434                                        if (jwkSetCache.requiresRefresh() || jwkSet == null) {
435                                                // Retrieve JWK set from URL
436                                                jwkSet = updateJWKSetFromURL();
437                                        }
438                                }
439                        } catch (Exception e) {
440                                
441                                List<JWK> failoverMatches = failover(e, jwkSelector, context);
442                                if (failoverMatches != null) {
443                                        return failoverMatches; // Failover success
444                                }
445                                
446                                if (jwkSet == null) {
447                                        // Rethrow the received exception if expired
448                                        throw e;
449                                }
450                                
451                                // Continue with cached version
452                        }
453                }
454
455                // Run the selector on the JWK set
456                List<JWK> matches = jwkSelector.select(jwkSet);
457
458                if (! matches.isEmpty()) {
459                        // Success
460                        return matches;
461                }
462
463                // Refresh the JWK set if the sought key ID is not in the cached JWK set
464
465                // Looking for JWK with specific ID?
466                String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher());
467                if (soughtKeyID == null) {
468                        // No key ID specified, return no matches
469                        return Collections.emptyList();
470                }
471
472                if (jwkSet.getKeyByKeyId(soughtKeyID) != null) {
473                        // The key ID exists in the cached JWK set, matching
474                        // failed for some other reason, return no matches
475                        return Collections.emptyList();
476                }
477                
478                try {
479                        // If the jwkSet in the cache is not the same instance that was
480                        // in the cache at the beginning of this method, then we know
481                        // the cache was updated
482                        synchronized (this) {
483                                if (jwkSet == jwkSetCache.get()) {
484                                        // Make new HTTP GET to the JWK set URL
485                                        jwkSet = updateJWKSetFromURL();
486                                } else {
487                                        // Cache was updated recently, the cached value is up-to-date
488                                        jwkSet = jwkSetCache.get();
489                                }
490                        }
491                } catch (KeySourceException e) {
492                        
493                        List<JWK> failoverMatches = failover(e, jwkSelector, context);
494                        if (failoverMatches != null) {
495                                return failoverMatches; // Failover success
496                        }
497                        
498                        throw e;
499                }
500                
501                
502                if (jwkSet == null) {
503                        // Retrieval has failed
504                        return Collections.emptyList();
505                }
506
507                // Repeat select, return final result (success or no matches)
508                return jwkSelector.select(jwkSet);
509        }
510}