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.KeySourceException; 030import com.nimbusds.jose.RemoteKeySourceException; 031import com.nimbusds.jose.jwk.JWK; 032import com.nimbusds.jose.jwk.JWKMatcher; 033import com.nimbusds.jose.jwk.JWKSelector; 034import com.nimbusds.jose.jwk.JWKSet; 035import com.nimbusds.jose.proc.SecurityContext; 036import com.nimbusds.jose.util.DefaultResourceRetriever; 037import com.nimbusds.jose.util.Resource; 038import com.nimbusds.jose.util.ResourceRetriever; 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 2022-01-30 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 if (jwkSetURL == null) { 273 throw new IllegalArgumentException("The JWK set URL must not be null"); 274 } 275 this.jwkSetURL = jwkSetURL; 276 277 this.failoverJWKSource = failoverJWKSource; 278 279 if (resourceRetriever != null) { 280 jwkSetRetriever = resourceRetriever; 281 } else { 282 jwkSetRetriever = new DefaultResourceRetriever( 283 resolveDefaultHTTPConnectTimeout(), 284 resolveDefaultHTTPReadTimeout(), 285 resolveDefaultHTTPSizeLimit()); 286 } 287 288 if (jwkSetCache != null) { 289 this.jwkSetCache = jwkSetCache; 290 } else { 291 this.jwkSetCache = new DefaultJWKSetCache(); 292 } 293 } 294 295 296 /** 297 * Updates the cached JWK set from the configured URL. 298 * 299 * @return The updated JWK set. 300 * 301 * @throws RemoteKeySourceException If JWK retrieval failed. 302 */ 303 private JWKSet updateJWKSetFromURL() 304 throws RemoteKeySourceException { 305 Resource res; 306 try { 307 res = jwkSetRetriever.retrieveResource(jwkSetURL); 308 } catch (IOException e) { 309 throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e); 310 } 311 JWKSet jwkSet; 312 try { 313 jwkSet = JWKSet.parse(res.getContent()); 314 } catch (java.text.ParseException e) { 315 throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e); 316 } 317 jwkSetCache.put(jwkSet); 318 return jwkSet; 319 } 320 321 322 /** 323 * Returns the JWK set URL. 324 * 325 * @return The JWK set URL. 326 */ 327 public URL getJWKSetURL() { 328 329 return jwkSetURL; 330 } 331 332 333 /** 334 * Returns the optional failover JWK source. 335 * 336 * @return The failover JWK source, {@code null} if not specified. 337 */ 338 public JWKSource<C> getFailoverJWKSource() { 339 340 return failoverJWKSource; 341 } 342 343 344 /** 345 * Returns the HTTP resource retriever. 346 * 347 * @return The HTTP resource retriever. 348 */ 349 public ResourceRetriever getResourceRetriever() { 350 351 return jwkSetRetriever; 352 } 353 354 355 /** 356 * Returns the configured JWK set cache. 357 * 358 * @return The JWK set cache. 359 */ 360 public JWKSetCache getJWKSetCache() { 361 362 return jwkSetCache; 363 } 364 365 366 /** 367 * Returns the cached JWK set. 368 * 369 * @return The cached JWK set, {@code null} if none or expired. 370 */ 371 public JWKSet getCachedJWKSet() { 372 373 return jwkSetCache.get(); 374 } 375 376 377 /** 378 * Returns the first specified key ID (kid) for a JWK matcher. 379 * 380 * @param jwkMatcher The JWK matcher. Must not be {@code null}. 381 * 382 * @return The first key ID, {@code null} if none. 383 */ 384 protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) { 385 386 Set<String> keyIDs = jwkMatcher.getKeyIDs(); 387 388 if (keyIDs == null || keyIDs.isEmpty()) { 389 return null; 390 } 391 392 for (String id: keyIDs) { 393 if (id != null) { 394 return id; 395 } 396 } 397 return null; // No kid in matcher 398 } 399 400 401 /** 402 * Fails over to the configuration optional JWK source. 403 */ 404 private List<JWK> failover(final Exception exception, final JWKSelector jwkSelector, final C context) 405 throws RemoteKeySourceException{ 406 407 if (getFailoverJWKSource() == null) { 408 return null; 409 } 410 411 try { 412 return getFailoverJWKSource().get(jwkSelector, context); 413 } catch (KeySourceException kse) { 414 throw new RemoteKeySourceException( 415 exception.getMessage() + 416 "; Failover JWK source retrieval failed with: " + kse.getMessage(), 417 kse 418 ); 419 } 420 } 421 422 423 @Override 424 public List<JWK> get(final JWKSelector jwkSelector, final C context) 425 throws RemoteKeySourceException { 426 427 // Check the cache first 428 JWKSet jwkSet = jwkSetCache.get(); 429 430 if (jwkSetCache.requiresRefresh() || jwkSet == null) { 431 // JWK set update required 432 try { 433 // Prevent multiple cache updates in case of concurrent requests 434 // (with double-checked locking, i.e. locking on update required only) 435 synchronized (this) { 436 jwkSet = jwkSetCache.get(); 437 if (jwkSetCache.requiresRefresh() || jwkSet == null) { 438 // Retrieve JWK set from URL 439 jwkSet = updateJWKSetFromURL(); 440 } 441 } 442 } catch (Exception e) { 443 444 List<JWK> failoverMatches = failover(e, jwkSelector, context); 445 if (failoverMatches != null) { 446 return failoverMatches; // Failover success 447 } 448 449 if (jwkSet == null) { 450 // Rethrow the received exception if expired 451 throw e; 452 } 453 454 // Continue with cached version 455 } 456 } 457 458 // Run the selector on the JWK set 459 List<JWK> matches = jwkSelector.select(jwkSet); 460 461 if (! matches.isEmpty()) { 462 // Success 463 return matches; 464 } 465 466 // Refresh the JWK set if the sought key ID is not in the cached JWK set 467 468 // Looking for JWK with specific ID? 469 String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher()); 470 if (soughtKeyID == null) { 471 // No key ID specified, return no matches 472 return Collections.emptyList(); 473 } 474 475 if (jwkSet.getKeyByKeyId(soughtKeyID) != null) { 476 // The key ID exists in the cached JWK set, matching 477 // failed for some other reason, return no matches 478 return Collections.emptyList(); 479 } 480 481 try { 482 // If the jwkSet in the cache is not the same instance that was 483 // in the cache at the beginning of this method, then we know 484 // the cache was updated 485 synchronized (this) { 486 if (jwkSet == jwkSetCache.get()) { 487 // Make new HTTP GET to the JWK set URL 488 jwkSet = updateJWKSetFromURL(); 489 } else { 490 // Cache was updated recently, the cached value is up-to-date 491 jwkSet = jwkSetCache.get(); 492 } 493 } 494 } catch (KeySourceException e) { 495 496 List<JWK> failoverMatches = failover(e, jwkSelector, context); 497 if (failoverMatches != null) { 498 return failoverMatches; // Failover success 499 } 500 501 throw e; 502 } 503 504 505 if (jwkSet == null) { 506 // Retrieval has failed 507 return Collections.emptyList(); 508 } 509 510 // Repeat select, return final result (success or no matches) 511 return jwkSelector.select(jwkSet); 512 } 513}