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