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