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