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