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