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