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 com.nimbusds.jose.RemoteKeySourceException;
028import com.nimbusds.jose.jwk.JWK;
029import com.nimbusds.jose.jwk.JWKMatcher;
030import com.nimbusds.jose.jwk.JWKSelector;
031import com.nimbusds.jose.jwk.JWKSet;
032import com.nimbusds.jose.proc.SecurityContext;
033import com.nimbusds.jose.util.DefaultResourceRetriever;
034import com.nimbusds.jose.util.Resource;
035import com.nimbusds.jose.util.ResourceRetriever;
036import net.jcip.annotations.ThreadSafe;
037
038
039/**
040 * Remote JSON Web Key (JWK) source specified by a JWK set URL. The retrieved
041 * JWK set is cached to minimise network calls. The cache is updated whenever
042 * the key selector tries to get a key with an unknown ID.
043 *
044 * @author Vladimir Dzhuvinov
045 * @version 2018-10-28
046 */
047@ThreadSafe
048public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> {
049
050
051        /**
052         * The default HTTP connect timeout for JWK set retrieval, in
053         * milliseconds. Set to 500 milliseconds.
054         */
055        public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 500;
056
057
058        /**
059         * The default HTTP read timeout for JWK set retrieval, in
060         * milliseconds. Set to 500 milliseconds.
061         */
062        public static final int DEFAULT_HTTP_READ_TIMEOUT = 500;
063
064
065        /**
066         * The default HTTP entity size limit for JWK set retrieval, in bytes.
067         * Set to 50 KBytes.
068         */
069        public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024;
070
071
072        /**
073         * The JWK set URL.
074         */
075        private final URL jwkSetURL;
076        
077
078        /**
079         * The JWK set cache.
080         */
081        private final JWKSetCache jwkSetCache;
082
083
084        /**
085         * The JWK set retriever.
086         */
087        private final ResourceRetriever jwkSetRetriever;
088
089
090        /**
091         * Creates a new remote JWK set using the
092         * {@link DefaultResourceRetriever default HTTP resource retriever},
093         * with a HTTP connect timeout set to 250 ms, HTTP read timeout set to
094         * 250 ms and a 50 KByte size limit.
095         *
096         * @param jwkSetURL The JWK set URL. Must not be {@code null}.
097         */
098        public RemoteJWKSet(final URL jwkSetURL) {
099                this(jwkSetURL, null);
100        }
101
102
103        /**
104         * Creates a new remote JWK set.
105         *
106         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
107         * @param resourceRetriever The HTTP resource retriever to use,
108         *                          {@code null} to use the
109         *                          {@link DefaultResourceRetriever default
110         *                          one}.
111         */
112        public RemoteJWKSet(final URL jwkSetURL,
113                            final ResourceRetriever resourceRetriever) {
114                
115                this(jwkSetURL, resourceRetriever, null);
116        }
117
118
119        /**
120         * Creates a new remote JWK set.
121         *
122         * @param jwkSetURL         The JWK set URL. Must not be {@code null}.
123         * @param resourceRetriever The HTTP resource retriever to use,
124         *                          {@code null} to use the
125         *                          {@link DefaultResourceRetriever default
126         *                          one}.
127         * @param jwkSetCache       The JWK set cache to use, {@code null} to
128         *                          use the {@link DefaultJWKSetCache default
129         *                          one}.
130         */
131        public RemoteJWKSet(final URL jwkSetURL,
132                            final ResourceRetriever resourceRetriever,
133                            final JWKSetCache jwkSetCache) {
134                
135                if (jwkSetURL == null) {
136                        throw new IllegalArgumentException("The JWK set URL must not be null");
137                }
138                this.jwkSetURL = jwkSetURL;
139
140                if (resourceRetriever != null) {
141                        jwkSetRetriever = resourceRetriever;
142                } else {
143                        jwkSetRetriever = new DefaultResourceRetriever(DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT, DEFAULT_HTTP_SIZE_LIMIT);
144                }
145                
146                if (jwkSetCache != null) {
147                        this.jwkSetCache = jwkSetCache;
148                } else {
149                        this.jwkSetCache = new DefaultJWKSetCache();
150                }
151        }
152
153
154        /**
155         * Updates the cached JWK set from the configured URL.
156         *
157         * @return The updated JWK set.
158         *
159         * @throws RemoteKeySourceException If JWK retrieval failed.
160         */
161        private JWKSet updateJWKSetFromURL()
162                throws RemoteKeySourceException {
163                Resource res;
164                try {
165                        res = jwkSetRetriever.retrieveResource(jwkSetURL);
166                } catch (IOException e) {
167                        throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e);
168                }
169                JWKSet jwkSet;
170                try {
171                        jwkSet = JWKSet.parse(res.getContent());
172                } catch (java.text.ParseException e) {
173                        throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e);
174                }
175                jwkSetCache.put(jwkSet);
176                return jwkSet;
177        }
178
179
180        /**
181         * Returns the JWK set URL.
182         *
183         * @return The JWK set URL.
184         */
185        public URL getJWKSetURL() {
186                
187                return jwkSetURL;
188        }
189
190
191        /**
192         * Returns the HTTP resource retriever.
193         *
194         * @return The HTTP resource retriever.
195         */
196        public ResourceRetriever getResourceRetriever() {
197
198                return jwkSetRetriever;
199        }
200        
201        
202        /**
203         * Returns the configured JWK set cache.
204         *
205         * @return The JWK set cache.
206         */
207        public JWKSetCache getJWKSetCache() {
208                
209                return jwkSetCache;
210        }
211        
212        
213        /**
214         * Returns the cached JWK set.
215         *
216         * @return The cached JWK set, {@code null} if none or expired.
217         */
218        public JWKSet getCachedJWKSet() {
219                
220                return jwkSetCache.get();
221        }
222
223
224        /**
225         * Returns the first specified key ID (kid) for a JWK matcher.
226         *
227         * @param jwkMatcher The JWK matcher. Must not be {@code null}.
228         *
229         * @return The first key ID, {@code null} if none.
230         */
231        protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) {
232
233                Set<String> keyIDs = jwkMatcher.getKeyIDs();
234
235                if (keyIDs == null || keyIDs.isEmpty()) {
236                        return null;
237                }
238
239                for (String id: keyIDs) {
240                        if (id != null) {
241                                return id;
242                        }
243                }
244                return null; // No kid in matcher
245        }
246
247
248        /**
249         * {@inheritDoc} The security context is ignored.
250         */
251        @Override
252        public List<JWK> get(final JWKSelector jwkSelector, final C context)
253                throws RemoteKeySourceException {
254
255                // Get the JWK set, may necessitate a cache update
256                JWKSet jwkSet = jwkSetCache.get();
257                if (jwkSetCache.requiresRefresh() || jwkSet == null) {
258                        try {
259                                // retrieve jwkSet by calling JWK set URL
260                                jwkSet = updateJWKSetFromURL();
261                        } catch (Exception ex) {
262                                if (jwkSet == null) {
263                                        // throw the received exception if expired.
264                                        throw  ex;
265                                }
266                        }
267                }
268
269                // Run the selector on the JWK set
270                List<JWK> matches = jwkSelector.select(jwkSet);
271
272                if (! matches.isEmpty()) {
273                        // Success
274                        return matches;
275                }
276
277                // Refresh the JWK set if the sought key ID is not in the cached JWK set
278
279                // Looking for JWK with specific ID?
280                String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher());
281                if (soughtKeyID == null) {
282                        // No key ID specified, return no matches
283                        return Collections.emptyList();
284                }
285
286                if (jwkSet.getKeyByKeyId(soughtKeyID) != null) {
287                        // The key ID exists in the cached JWK set, matching
288                        // failed for some other reason, return no matches
289                        return Collections.emptyList();
290                }
291
292                // Make new HTTP GET to the JWK set URL
293                jwkSet = updateJWKSetFromURL();
294                if (jwkSet == null) {
295                        // Retrieval has failed
296                        return Collections.emptyList();
297                }
298
299                // Repeat select, return final result (success or no matches)
300                return jwkSelector.select(jwkSet);
301        }
302}