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