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 * @version 2022-01-24
069 */
070@ThreadSafe
071public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> {
072
073
074        /**
075         * The default HTTP connect timeout for JWK set retrieval, in
076         * milliseconds. Set to 500 milliseconds.
077         */
078        public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 500;
079
080
081        /**
082         * The default HTTP read timeout for JWK set retrieval, in
083         * milliseconds. Set to 500 milliseconds.
084         */
085        public static final int DEFAULT_HTTP_READ_TIMEOUT = 500;
086
087
088        /**
089         * The default HTTP entity size limit for JWK set retrieval, in bytes.
090         * Set to 50 KBytes.
091         */
092        public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024;
093        
094        
095        /**
096         * Resolves the default HTTP connect timeout for JWK set retrieval, in
097         * milliseconds.
098         *
099         * @return The {@link #DEFAULT_HTTP_CONNECT_TIMEOUT static constant},
100         *         overridden by setting the
101         *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout}
102         *         Java system property.
103         */
104        public static int resolveDefaultHTTPConnectTimeout() {
105                return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpConnectTimeout", DEFAULT_HTTP_CONNECT_TIMEOUT);
106        }
107        
108        
109        /**
110         * Resolves the default HTTP read timeout for JWK set retrieval, in
111         * milliseconds.
112         *
113         * @return The {@link #DEFAULT_HTTP_READ_TIMEOUT static constant},
114         *         overridden by setting the
115         *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout}
116         *         Java system property.
117         */
118        public static int resolveDefaultHTTPReadTimeout() {
119                return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpReadTimeout", DEFAULT_HTTP_READ_TIMEOUT);
120        }
121        
122        
123        /**
124         * Resolves default HTTP entity size limit for JWK set retrieval, in
125         * bytes.
126         *
127         * @return The {@link #DEFAULT_HTTP_SIZE_LIMIT static constant},
128         *         overridden by setting the
129         *         {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit}
130         *         Java system property.
131         */
132        public static int resolveDefaultHTTPSizeLimit() {
133                return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpSizeLimit", DEFAULT_HTTP_SIZE_LIMIT);
134        }
135        
136        
137        private static int resolveDefault(final String sysPropertyName, final int defaultValue) {
138                
139                String value = System.getProperty(sysPropertyName);
140                
141                if (value == null) {
142                        return defaultValue;
143                }
144                
145                try {
146                        return Integer.parseInt(value);
147                } catch (NumberFormatException e) {
148                        // Illegal value
149                        return defaultValue;
150                }
151        }
152
153
154        /**
155         * The JWK set URL.
156         */
157        private final URL jwkSetURL;
158        
159
160        /**
161         * The JWK set cache.
162         */
163        private final JWKSetCache jwkSetCache;
164
165
166        /**
167         * The JWK set retriever.
168         */
169        private final ResourceRetriever jwkSetRetriever;
170
171
172        /**
173         * Creates a new remote JWK set using the
174         * {@link DefaultResourceRetriever default HTTP resource retriever}
175         * with the default HTTP timeouts and entity size limit.
176         *
177         * @param jwkSetURL The JWK set URL. Must not be {@code null}.
178         */
179        public RemoteJWKSet(final URL jwkSetURL) {
180                this(jwkSetURL, null);
181        }
182
183
184        /**
185         * Creates a new remote JWK set.
186         *
187         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
188         * @param resourceRetriever The HTTP resource retriever to use,
189         *                          {@code null} to use the
190         *                          {@link DefaultResourceRetriever default
191         *                          one} with the default HTTP timeouts and
192         *                          entity size limit.
193         */
194        public RemoteJWKSet(final URL jwkSetURL,
195                            final ResourceRetriever resourceRetriever) {
196                
197                this(jwkSetURL, resourceRetriever, null);
198        }
199
200
201        /**
202         * Creates a new remote JWK set.
203         *
204         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
205         * @param resourceRetriever The HTTP resource retriever to use,
206         *                          {@code null} to use the
207         *                          {@link DefaultResourceRetriever default
208         *                          one} with the default HTTP timeouts and
209         *                          entity size limit.
210         * @param jwkSetCache       The JWK set cache to use, {@code null} to
211         *                          use the {@link DefaultJWKSetCache default
212         *                          one}.
213         */
214        public RemoteJWKSet(final URL jwkSetURL,
215                            final ResourceRetriever resourceRetriever,
216                            final JWKSetCache jwkSetCache) {
217                
218                if (jwkSetURL == null) {
219                        throw new IllegalArgumentException("The JWK set URL must not be null");
220                }
221                this.jwkSetURL = jwkSetURL;
222
223                if (resourceRetriever != null) {
224                        jwkSetRetriever = resourceRetriever;
225                } else {
226                        jwkSetRetriever = new DefaultResourceRetriever(
227                                resolveDefaultHTTPConnectTimeout(),
228                                resolveDefaultHTTPReadTimeout(),
229                                resolveDefaultHTTPSizeLimit());
230                }
231                
232                if (jwkSetCache != null) {
233                        this.jwkSetCache = jwkSetCache;
234                } else {
235                        this.jwkSetCache = new DefaultJWKSetCache();
236                }
237        }
238
239
240        /**
241         * Updates the cached JWK set from the configured URL.
242         *
243         * @return The updated JWK set.
244         *
245         * @throws RemoteKeySourceException If JWK retrieval failed.
246         */
247        private JWKSet updateJWKSetFromURL()
248                throws RemoteKeySourceException {
249                Resource res;
250                try {
251                        res = jwkSetRetriever.retrieveResource(jwkSetURL);
252                } catch (IOException e) {
253                        throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e);
254                }
255                JWKSet jwkSet;
256                try {
257                        jwkSet = JWKSet.parse(res.getContent());
258                } catch (java.text.ParseException e) {
259                        throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e);
260                }
261                jwkSetCache.put(jwkSet);
262                return jwkSet;
263        }
264
265
266        /**
267         * Returns the JWK set URL.
268         *
269         * @return The JWK set URL.
270         */
271        public URL getJWKSetURL() {
272                
273                return jwkSetURL;
274        }
275
276
277        /**
278         * Returns the HTTP resource retriever.
279         *
280         * @return The HTTP resource retriever.
281         */
282        public ResourceRetriever getResourceRetriever() {
283
284                return jwkSetRetriever;
285        }
286        
287        
288        /**
289         * Returns the configured JWK set cache.
290         *
291         * @return The JWK set cache.
292         */
293        public JWKSetCache getJWKSetCache() {
294                
295                return jwkSetCache;
296        }
297        
298        
299        /**
300         * Returns the cached JWK set.
301         *
302         * @return The cached JWK set, {@code null} if none or expired.
303         */
304        public JWKSet getCachedJWKSet() {
305                
306                return jwkSetCache.get();
307        }
308
309
310        /**
311         * Returns the first specified key ID (kid) for a JWK matcher.
312         *
313         * @param jwkMatcher The JWK matcher. Must not be {@code null}.
314         *
315         * @return The first key ID, {@code null} if none.
316         */
317        protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) {
318
319                Set<String> keyIDs = jwkMatcher.getKeyIDs();
320
321                if (keyIDs == null || keyIDs.isEmpty()) {
322                        return null;
323                }
324
325                for (String id: keyIDs) {
326                        if (id != null) {
327                                return id;
328                        }
329                }
330                return null; // No kid in matcher
331        }
332
333
334        /**
335         * {@inheritDoc} The security context is ignored.
336         */
337        @Override
338        public List<JWK> get(final JWKSelector jwkSelector, final C context)
339                throws RemoteKeySourceException {
340
341                // Get the JWK set, may necessitate a cache update
342                JWKSet jwkSet = jwkSetCache.get();
343                if (jwkSetCache.requiresRefresh() || jwkSet == null) {
344                        try {
345                                // retrieve jwkSet by calling JWK set URL
346                                jwkSet = updateJWKSetFromURL();
347                        } catch (Exception ex) {
348                                if (jwkSet == null) {
349                                        // throw the received exception if expired.
350                                        throw  ex;
351                                }
352                        }
353                }
354
355                // Run the selector on the JWK set
356                List<JWK> matches = jwkSelector.select(jwkSet);
357
358                if (! matches.isEmpty()) {
359                        // Success
360                        return matches;
361                }
362
363                // Refresh the JWK set if the sought key ID is not in the cached JWK set
364
365                // Looking for JWK with specific ID?
366                String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher());
367                if (soughtKeyID == null) {
368                        // No key ID specified, return no matches
369                        return Collections.emptyList();
370                }
371
372                if (jwkSet.getKeyByKeyId(soughtKeyID) != null) {
373                        // The key ID exists in the cached JWK set, matching
374                        // failed for some other reason, return no matches
375                        return Collections.emptyList();
376                }
377
378                // Make new HTTP GET to the JWK set URL
379                jwkSet = updateJWKSetFromURL();
380                if (jwkSet == null) {
381                        // Retrieval has failed
382                        return Collections.emptyList();
383                }
384
385                // Repeat select, return final result (success or no matches)
386                return jwkSelector.select(jwkSet);
387        }
388}