001/* 002 * nimbus-jose-jwt 003 * 004 * Copyright 2012-2016, Connect2id Ltd. 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.jose.jwk.source; 019 020 021import java.io.IOException; 022import java.net.URL; 023import java.util.Collections; 024import java.util.List; 025import java.util.Set; 026 027import net.jcip.annotations.ThreadSafe; 028 029import com.nimbusds.jose.RemoteKeySourceException; 030import com.nimbusds.jose.jwk.JWK; 031import com.nimbusds.jose.jwk.JWKMatcher; 032import com.nimbusds.jose.jwk.JWKSelector; 033import com.nimbusds.jose.jwk.JWKSet; 034import com.nimbusds.jose.proc.SecurityContext; 035import com.nimbusds.jose.util.DefaultResourceRetriever; 036import com.nimbusds.jose.util.Resource; 037import com.nimbusds.jose.util.ResourceRetriever; 038 039 040/** 041 * Remote JSON Web Key (JWK) source specified by a JWK set URL. The retrieved 042 * JWK set is cached to minimise network calls. The cache is updated whenever 043 * the key selector tries to get a key with an unknown ID. 044 * 045 * <p>If no {@link ResourceRetriever} is specified when creating a remote JWK 046 * set source the {@link DefaultResourceRetriever default one} will be used, 047 * with the following HTTP timeouts and limits: 048 * 049 * <ul> 050 * <li>HTTP connect timeout, in milliseconds: Determined by the 051 * {@link #DEFAULT_HTTP_CONNECT_TIMEOUT} constant which can be 052 * overridden by setting the 053 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout} 054 * Java system property. 055 * <li>HTTP read timeout, in milliseconds: Determined by the 056 * {@link #DEFAULT_HTTP_READ_TIMEOUT} constant which can be 057 * overridden by setting the 058 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout} 059 * Java system property. 060 * <li>HTTP entity size limit: Determined by the 061 * {@link #DEFAULT_HTTP_SIZE_LIMIT} constant which can be 062 * overridden by setting the 063 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit} 064 * Java system property. 065 * </ul> 066 * 067 * @author Vladimir Dzhuvinov 068 * @author Andreas Huber 069 * @version 2022-01-24 070 */ 071@ThreadSafe 072public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> { 073 074 075 /** 076 * The default HTTP connect timeout for JWK set retrieval, in 077 * milliseconds. Set to 500 milliseconds. 078 */ 079 public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 500; 080 081 082 /** 083 * The default HTTP read timeout for JWK set retrieval, in 084 * milliseconds. Set to 500 milliseconds. 085 */ 086 public static final int DEFAULT_HTTP_READ_TIMEOUT = 500; 087 088 089 /** 090 * The default HTTP entity size limit for JWK set retrieval, in bytes. 091 * Set to 50 KBytes. 092 */ 093 public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024; 094 095 096 /** 097 * Resolves the default HTTP connect timeout for JWK set retrieval, in 098 * milliseconds. 099 * 100 * @return The {@link #DEFAULT_HTTP_CONNECT_TIMEOUT static constant}, 101 * overridden by setting the 102 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout} 103 * Java system property. 104 */ 105 public static int resolveDefaultHTTPConnectTimeout() { 106 return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpConnectTimeout", DEFAULT_HTTP_CONNECT_TIMEOUT); 107 } 108 109 110 /** 111 * Resolves the default HTTP read timeout for JWK set retrieval, in 112 * milliseconds. 113 * 114 * @return The {@link #DEFAULT_HTTP_READ_TIMEOUT static constant}, 115 * overridden by setting the 116 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout} 117 * Java system property. 118 */ 119 public static int resolveDefaultHTTPReadTimeout() { 120 return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpReadTimeout", DEFAULT_HTTP_READ_TIMEOUT); 121 } 122 123 124 /** 125 * Resolves default HTTP entity size limit for JWK set retrieval, in 126 * bytes. 127 * 128 * @return The {@link #DEFAULT_HTTP_SIZE_LIMIT static constant}, 129 * overridden by setting the 130 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit} 131 * Java system property. 132 */ 133 public static int resolveDefaultHTTPSizeLimit() { 134 return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpSizeLimit", DEFAULT_HTTP_SIZE_LIMIT); 135 } 136 137 138 private static int resolveDefault(final String sysPropertyName, final int defaultValue) { 139 140 String value = System.getProperty(sysPropertyName); 141 142 if (value == null) { 143 return defaultValue; 144 } 145 146 try { 147 return Integer.parseInt(value); 148 } catch (NumberFormatException e) { 149 // Illegal value 150 return defaultValue; 151 } 152 } 153 154 155 /** 156 * The JWK set URL. 157 */ 158 private final URL jwkSetURL; 159 160 161 /** 162 * The JWK set cache. 163 */ 164 private final JWKSetCache jwkSetCache; 165 166 167 /** 168 * The JWK set retriever. 169 */ 170 private final ResourceRetriever jwkSetRetriever; 171 172 173 /** 174 * Creates a new remote JWK set using the 175 * {@link DefaultResourceRetriever default HTTP resource retriever} 176 * with the default HTTP timeouts and entity size limit. 177 * 178 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 179 */ 180 public RemoteJWKSet(final URL jwkSetURL) { 181 this(jwkSetURL, null); 182 } 183 184 185 /** 186 * Creates a new remote JWK set. 187 * 188 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 189 * @param resourceRetriever The HTTP resource retriever to use, 190 * {@code null} to use the 191 * {@link DefaultResourceRetriever default 192 * one} with the default HTTP timeouts and 193 * entity size limit. 194 */ 195 public RemoteJWKSet(final URL jwkSetURL, 196 final ResourceRetriever resourceRetriever) { 197 198 this(jwkSetURL, resourceRetriever, null); 199 } 200 201 202 /** 203 * Creates a new remote JWK set. 204 * 205 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 206 * @param resourceRetriever The HTTP resource retriever to use, 207 * {@code null} to use the 208 * {@link DefaultResourceRetriever default 209 * one} with the default HTTP timeouts and 210 * entity size limit. 211 * @param jwkSetCache The JWK set cache to use, {@code null} to 212 * use the {@link DefaultJWKSetCache default 213 * one}. 214 */ 215 public RemoteJWKSet(final URL jwkSetURL, 216 final ResourceRetriever resourceRetriever, 217 final JWKSetCache jwkSetCache) { 218 219 if (jwkSetURL == null) { 220 throw new IllegalArgumentException("The JWK set URL must not be null"); 221 } 222 this.jwkSetURL = jwkSetURL; 223 224 if (resourceRetriever != null) { 225 jwkSetRetriever = resourceRetriever; 226 } else { 227 jwkSetRetriever = new DefaultResourceRetriever( 228 resolveDefaultHTTPConnectTimeout(), 229 resolveDefaultHTTPReadTimeout(), 230 resolveDefaultHTTPSizeLimit()); 231 } 232 233 if (jwkSetCache != null) { 234 this.jwkSetCache = jwkSetCache; 235 } else { 236 this.jwkSetCache = new DefaultJWKSetCache(); 237 } 238 } 239 240 241 /** 242 * Updates the cached JWK set from the configured URL. 243 * 244 * @return The updated JWK set. 245 * 246 * @throws RemoteKeySourceException If JWK retrieval failed. 247 */ 248 private JWKSet updateJWKSetFromURL() 249 throws RemoteKeySourceException { 250 Resource res; 251 try { 252 res = jwkSetRetriever.retrieveResource(jwkSetURL); 253 } catch (IOException e) { 254 throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e); 255 } 256 JWKSet jwkSet; 257 try { 258 jwkSet = JWKSet.parse(res.getContent()); 259 } catch (java.text.ParseException e) { 260 throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e); 261 } 262 jwkSetCache.put(jwkSet); 263 return jwkSet; 264 } 265 266 267 /** 268 * Returns the JWK set URL. 269 * 270 * @return The JWK set URL. 271 */ 272 public URL getJWKSetURL() { 273 274 return jwkSetURL; 275 } 276 277 278 /** 279 * Returns the HTTP resource retriever. 280 * 281 * @return The HTTP resource retriever. 282 */ 283 public ResourceRetriever getResourceRetriever() { 284 285 return jwkSetRetriever; 286 } 287 288 289 /** 290 * Returns the configured JWK set cache. 291 * 292 * @return The JWK set cache. 293 */ 294 public JWKSetCache getJWKSetCache() { 295 296 return jwkSetCache; 297 } 298 299 300 /** 301 * Returns the cached JWK set. 302 * 303 * @return The cached JWK set, {@code null} if none or expired. 304 */ 305 public JWKSet getCachedJWKSet() { 306 307 return jwkSetCache.get(); 308 } 309 310 311 /** 312 * Returns the first specified key ID (kid) for a JWK matcher. 313 * 314 * @param jwkMatcher The JWK matcher. Must not be {@code null}. 315 * 316 * @return The first key ID, {@code null} if none. 317 */ 318 protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) { 319 320 Set<String> keyIDs = jwkMatcher.getKeyIDs(); 321 322 if (keyIDs == null || keyIDs.isEmpty()) { 323 return null; 324 } 325 326 for (String id: keyIDs) { 327 if (id != null) { 328 return id; 329 } 330 } 331 return null; // No kid in matcher 332 } 333 334 335 /** 336 * {@inheritDoc} The security context is ignored. 337 */ 338 @Override 339 public List<JWK> get(final JWKSelector jwkSelector, final C context) 340 throws RemoteKeySourceException { 341 342 // Get the JWK set, may necessitate a cache update. 343 JWKSet jwkSet = jwkSetCache.get(); 344 if (jwkSetCache.requiresRefresh() || jwkSet == null) { 345 try { 346 // Prevent multiple cache updates in case of concurrent requests 347 // (with double-checked locking / locking on update required only) 348 synchronized (this) { 349 jwkSet = jwkSetCache.get(); 350 if (jwkSetCache.requiresRefresh() || jwkSet == null) { 351 // Retrieve jwkSet by calling JWK set URL 352 jwkSet = updateJWKSetFromURL(); 353 } 354 } 355 } catch (Exception ex) { 356 if (jwkSet == null) { 357 // Rethrow the received exception if expired 358 throw ex; 359 } 360 } 361 } 362 363 // Run the selector on the JWK set 364 List<JWK> matches = jwkSelector.select(jwkSet); 365 366 if (! matches.isEmpty()) { 367 // Success 368 return matches; 369 } 370 371 // Refresh the JWK set if the sought key ID is not in the cached JWK set 372 373 // Looking for JWK with specific ID? 374 String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher()); 375 if (soughtKeyID == null) { 376 // No key ID specified, return no matches 377 return Collections.emptyList(); 378 } 379 380 if (jwkSet.getKeyByKeyId(soughtKeyID) != null) { 381 // The key ID exists in the cached JWK set, matching 382 // failed for some other reason, return no matches 383 return Collections.emptyList(); 384 } 385 386 // If the jwkSet in the cache is not the same instance that was 387 // in the cache at the beginning of this method, then we know 388 // the cache was updated 389 synchronized (this) { 390 if (jwkSet == jwkSetCache.get()) { 391 // Make new HTTP GET to the JWK set URL 392 jwkSet = updateJWKSetFromURL(); 393 } else { 394 // Cache was updated recently, the cached value is up-to-date 395 jwkSet = jwkSetCache.get(); 396 } 397 } 398 399 if (jwkSet == null) { 400 // Retrieval has failed 401 return Collections.emptyList(); 402 } 403 404 // Repeat select, return final result (success or no matches) 405 return jwkSelector.select(jwkSet); 406 } 407}