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