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.RestrictedResourceRetriever;
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.
025 *
026 * @author Vladimir Dzhuvinov
027 * @version 2016-04-10
028 */
029@ThreadSafe
030public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> {
031
032
033        /**
034         * The default HTTP connect timeout for JWK set retrieval, in
035         * milliseconds. Set to 250 milliseconds.
036         */
037        public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 250;
038
039
040        /**
041         * The default HTTP read timeout for JWK set retrieval, in
042         * milliseconds. Set to 250 milliseconds.
043         */
044        public static final int DEFAULT_HTTP_READ_TIMEOUT = 250;
045
046
047        /**
048         * The default HTTP entity size limit for JWK set retrieval, in bytes.
049         * Set to 50 KBytes.
050         */
051        public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024;
052
053
054        /**
055         * The JWK set URL.
056         */
057        private final URL jwkSetURL;
058        
059
060        /**
061         * The cached JWK set.
062         */
063        private final AtomicReference<JWKSet> cachedJWKSet = new AtomicReference<>();
064
065
066        /**
067         * The JWK set retriever.
068         */
069        private final RestrictedResourceRetriever jwkSetRetriever;
070
071
072        /**
073         * Creates a new remote JWK set using the
074         * {@link DefaultResourceRetriever default HTTP resource retriever}.
075         * Starts an asynchronous thread to fetch the JWK set from the
076         * specified URL. The JWK set is cached if successfully retrieved.
077         *
078         * @param jwkSetURL The JWK set URL. Must not be {@code null}.
079         */
080        public RemoteJWKSet(final URL jwkSetURL) {
081                this(jwkSetURL, null);
082        }
083
084
085        /**
086         * Creates a new remote JWK set. Starts an asynchronous thread to
087         * fetch the JWK set from the specified URL. The JWK set is cached if
088         * successfully retrieved.
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 RestrictedResourceRetriever 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                Thread t = new Thread() {
110                        public void run() {
111                                updateJWKSetFromURL();
112                        }
113                };
114                t.setName("initial-jwk-set-retriever["+ jwkSetURL +"]");
115                t.start();
116        }
117
118
119        /**
120         * Updates the cached JWK set from the configured URL.
121         *
122         * @return The updated JWK set, {@code null} if retrieval failed.
123         */
124        private JWKSet updateJWKSetFromURL() {
125                JWKSet jwkSet;
126                try {
127                        Resource res = jwkSetRetriever.retrieveResource(jwkSetURL);
128                        jwkSet = JWKSet.parse(res.getContent());
129                } catch (IOException | java.text.ParseException e) {
130                        return null;
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 RestrictedResourceRetriever 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 getJWKSet() {
164                JWKSet jwkSet = cachedJWKSet.get();
165                if (jwkSet != null) {
166                        return jwkSet;
167                }
168                return updateJWKSetFromURL();
169        }
170
171
172        /**
173         * Returns the first specified key ID (kid) for a JWK matcher.
174         *
175         * @param jwkMatcher The JWK matcher. Must not be {@code null}.
176         *
177         * @return The first key ID, {@code null} if none.
178         */
179        protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) {
180
181                Set<String> keyIDs = jwkMatcher.getKeyIDs();
182
183                if (keyIDs == null || keyIDs.isEmpty()) {
184                        return null;
185                }
186
187                for (String id: keyIDs) {
188                        if (id != null) {
189                                return id;
190                        }
191                }
192                return null; // No kid in matcher
193        }
194
195
196        /**
197         * {@inheritDoc} The security context is ignored.
198         */
199        @Override
200        public List<JWK> get(final JWKSelector jwkSelector, final C context) {
201
202                // Get the JWK set, may necessitate a cache update
203                JWKSet jwkSet = getJWKSet();
204                if (jwkSet == null) {
205                        // Retrieval has failed
206                        return Collections.emptyList();
207                }
208                List<JWK> matches = jwkSelector.select(jwkSet);
209
210                if (! matches.isEmpty()) {
211                        // Success
212                        return matches;
213                }
214
215                // Refresh the JWK set if the sought key ID is not in the cached JWK set
216                String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher());
217                if (soughtKeyID == null) {
218                        // No key ID specified, return no matches
219                        return matches;
220                }
221                if (jwkSet.getKeyByKeyId(soughtKeyID) != null) {
222                        // The key ID exists in the cached JWK set, matching
223                        // failed for some other reason, return no matches
224                        return matches;
225                }
226                // Make new HTTP GET to the JWK set URL
227                jwkSet = updateJWKSetFromURL();
228                if (jwkSet == null) {
229                        // Retrieval has failed
230                        return null;
231                }
232                // Repeat select, return final result (success or no matches)
233                return jwkSelector.select(jwkSet);
234        }
235}