001/* 002 * nimbus-jose-jwt 003 * 004 * Copyright 2012-2022, 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.util.Objects; 023import java.util.concurrent.*; 024import java.util.concurrent.locks.ReentrantLock; 025 026import net.jcip.annotations.ThreadSafe; 027 028import com.nimbusds.jose.KeySourceException; 029import com.nimbusds.jose.jwk.JWKSet; 030import com.nimbusds.jose.proc.SecurityContext; 031import com.nimbusds.jose.util.cache.CachedObject; 032import com.nimbusds.jose.util.events.EventListener; 033 034 035/** 036 * Caching {@linkplain JWKSetSource} that refreshes the JWK set prior to its 037 * expiration. The updates run on a separate, dedicated thread. Updates can be 038 * repeatedly scheduled, or (lazily) triggered by incoming requests for the JWK 039 * set. 040 * 041 * <p>This class is intended for uninterrupted operation under high-load, to 042 * avoid a potentially large number of threads blocking when the cache expires 043 * (and must be refreshed). 044 * 045 * @author Thomas Rørvik Skjølberg 046 * @author Vladimir Dzhuvinov 047 * @version 2025-06-24 048 */ 049@ThreadSafe 050public class RefreshAheadCachingJWKSetSource<C extends SecurityContext> extends CachingJWKSetSource<C> { 051 052 053 /** 054 * New JWK set refresh scheduled event. 055 */ 056 public static class RefreshScheduledEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C> { 057 058 public RefreshScheduledEvent(final RefreshAheadCachingJWKSetSource<C> source, final C context) { 059 super(source, context); 060 } 061 } 062 063 064 /** 065 * JWK set refresh not scheduled event. 066 */ 067 public static class RefreshNotScheduledEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C> { 068 069 public RefreshNotScheduledEvent(final RefreshAheadCachingJWKSetSource<C> source, final C context) { 070 super(source, context); 071 } 072 } 073 074 075 /** 076 * Scheduled JWK refresh failed event. 077 */ 078 public static class ScheduledRefreshFailed<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C> { 079 080 private final Exception exception; 081 082 public ScheduledRefreshFailed(final CachingJWKSetSource<C> source, 083 final Exception exception, 084 final C context) { 085 super(source, context); 086 Objects.requireNonNull(exception); 087 this.exception = exception; 088 } 089 090 091 public Exception getException() { 092 return exception; 093 } 094 } 095 096 097 /** 098 * Scheduled JWK set cache refresh initiated event. 099 */ 100 public static class ScheduledRefreshInitiatedEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C> { 101 102 private ScheduledRefreshInitiatedEvent(final CachingJWKSetSource<C> source, final C context) { 103 super(source, context); 104 } 105 } 106 107 108 /** 109 * Scheduled JWK set cache refresh completed event. 110 */ 111 public static class ScheduledRefreshCompletedEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C> { 112 113 private final JWKSet jwkSet; 114 115 private ScheduledRefreshCompletedEvent(final CachingJWKSetSource<C> source, 116 final JWKSet jwkSet, 117 final C context) { 118 super(source, context); 119 Objects.requireNonNull(jwkSet); 120 this.jwkSet = jwkSet; 121 } 122 123 124 /** 125 * Returns the refreshed JWK set. 126 * 127 * @return The refreshed JWK set. 128 */ 129 public JWKSet getJWKSet() { 130 return jwkSet; 131 } 132 } 133 134 135 /** 136 * Unable to refresh the JWK set cache ahead of expiration event. 137 */ 138 public static class UnableToRefreshAheadOfExpirationEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<CachingJWKSetSource<C>, C> { 139 140 141 public UnableToRefreshAheadOfExpirationEvent(final CachingJWKSetSource<C> source, final C context) { 142 super(source, context); 143 } 144 } 145 146 147 /** 148 * Creates a new instance of the default {@link ExecutorService} 149 * implementation to refresh the cache. 150 */ 151 public static ExecutorService createDefaultExecutorService() { 152 return Executors.newSingleThreadExecutor(); 153 } 154 155 156 /** 157 * Creates a new instance of the default 158 * {@link ScheduledExecutorService} implementation to perform scheduled 159 * cache refreshes in the background. 160 */ 161 public static ScheduledExecutorService createDefaultScheduledExecutorService() { 162 return Executors.newSingleThreadScheduledExecutor(); 163 } 164 165 166 // refresh ahead of expiration should execute when 167 // expirationTime - refreshAheadTime < currentTime < expirationTime 168 private final long refreshAheadTime; // milliseconds 169 170 private final ReentrantLock lazyLock = new ReentrantLock(); 171 172 private final ExecutorService executorService; 173 private final boolean shutdownExecutorOnClose; 174 private final ScheduledExecutorService scheduledExecutorService; 175 private final boolean shutdownScheduledExecutorOnClose; 176 177 // cache expiration time (in milliseconds) used as fingerprint 178 private volatile long cacheExpiration; 179 180 private ScheduledFuture<?> scheduledRefreshFuture; 181 182 private final EventListener<CachingJWKSetSource<C>, C> eventListener; 183 184 185 /** 186 * Creates a new refresh-ahead caching JWK set source. 187 * 188 * @param source The JWK set source to decorate. Must not 189 * be {@code null}. 190 * @param timeToLive The time to live of the cached JWK set, 191 * in milliseconds. 192 * @param cacheRefreshTimeout The cache refresh timeout, in 193 * milliseconds. 194 * @param refreshAheadTime The refresh ahead time, in milliseconds. 195 * @param scheduled {@code true} to refresh in a scheduled 196 * manner, regardless of requests. 197 * @param eventListener The event listener, {@code null} if not 198 * specified. 199 */ 200 public RefreshAheadCachingJWKSetSource(final JWKSetSource<C> source, 201 final long timeToLive, 202 final long cacheRefreshTimeout, 203 final long refreshAheadTime, 204 final boolean scheduled, 205 final EventListener<CachingJWKSetSource<C>, C> eventListener) { 206 207 this(source, timeToLive, cacheRefreshTimeout, refreshAheadTime, 208 createDefaultExecutorService(), true, 209 eventListener, scheduled ? createDefaultScheduledExecutorService() : null, scheduled); 210 } 211 212 213 /** 214 * Creates a new refresh-ahead caching JWK set source with the 215 * specified executor service to run the updates in the background. 216 * 217 * @param source The JWK set source to decorate. Must 218 * not be {@code null}. 219 * @param timeToLive The time to live of the cached JWK 220 * set, in milliseconds. 221 * @param cacheRefreshTimeout The cache refresh timeout, in 222 * milliseconds. 223 * @param refreshAheadTime The refresh ahead time, in 224 * milliseconds. 225 * @param scheduled {@code true} to refresh in a 226 * scheduled manner, regardless of 227 * requests. 228 * @param executorService The executor service to run the 229 * updates in the background. 230 * @param shutdownExecutorOnClose If {@code true} the executor service 231 * will be shut down upon closing the 232 * source. 233 * @param eventListener The event listener, {@code null} if 234 * not specified. 235 */ 236 public RefreshAheadCachingJWKSetSource(final JWKSetSource<C> source, 237 final long timeToLive, 238 final long cacheRefreshTimeout, 239 final long refreshAheadTime, 240 final boolean scheduled, 241 final ExecutorService executorService, 242 final boolean shutdownExecutorOnClose, 243 final EventListener<CachingJWKSetSource<C>, C> eventListener) { 244 this(source, timeToLive, cacheRefreshTimeout, refreshAheadTime, executorService, shutdownExecutorOnClose, eventListener, scheduled ? createDefaultScheduledExecutorService() : null, scheduled); 245 } 246 247 248 /** 249 * Creates a new refresh-ahead caching JWK set source with the 250 * specified {@link ExecutorService} to run the updates in the 251 * background. The parameters include an optional 252 * {@link ScheduledExecutorService} to schedule the updates in the 253 * background. 254 * <p> 255 * Note about the {@link ScheduledExecutorService}: It is assumed 256 * that a thread will be available to schedule the update of the cache 257 * when needed. If this is not the case then the updates will not be 258 * scheduled on-time. This could, in the worst-case scenario, lead to 259 * the cache being expired when 260 * {@link #getJWKSet(JWKSetCacheRefreshEvaluator, long, SecurityContext)} 261 * is called. 262 * 263 * @param source The JWK set source to 264 * decorate. Must not be 265 * {@code null}. 266 * @param timeToLive The time to live of the 267 * cached JWK set, in 268 * milliseconds. 269 * @param cacheRefreshTimeout The cache refresh timeout, 270 * in milliseconds. 271 * @param refreshAheadTime The refresh ahead time, in 272 * milliseconds. 273 * @param executorService The executor service to run 274 * the updates in the 275 * background. 276 * @param shutdownExecutorOnClose If {@code true} the executor 277 * service will be shut down 278 * upon closing the source. 279 * @param eventListener The event listener, 280 * {@code null} if not specified. 281 * @param scheduledExecutorService The {@link ScheduledExecutorService} 282 * to schedule the updates in 283 * the background. If {@code null} 284 * no updates will be scheduled. 285 * @param shutdownScheduledExecutorOnClose If {@code true} then the 286 * {@link ScheduledExecutorService} 287 * will be shut down upon 288 * closing the source. 289 */ 290 public RefreshAheadCachingJWKSetSource(final JWKSetSource<C> source, 291 final long timeToLive, 292 final long cacheRefreshTimeout, 293 final long refreshAheadTime, 294 final ExecutorService executorService, 295 final boolean shutdownExecutorOnClose, 296 final EventListener<CachingJWKSetSource<C>, C> eventListener, 297 final ScheduledExecutorService scheduledExecutorService, 298 final boolean shutdownScheduledExecutorOnClose) { 299 300 super(source, timeToLive, cacheRefreshTimeout, eventListener); 301 302 if (refreshAheadTime + cacheRefreshTimeout > timeToLive) { 303 throw new IllegalArgumentException("The sum of the refresh-ahead time (" + refreshAheadTime +"ms) " + 304 "and the cache refresh timeout (" + cacheRefreshTimeout +"ms) " + 305 "must not exceed the time-to-lived time (" + timeToLive + "ms)"); 306 } 307 308 this.refreshAheadTime = refreshAheadTime; 309 310 Objects.requireNonNull(executorService, "The executor service must not be null"); 311 this.executorService = executorService; 312 313 this.shutdownExecutorOnClose = shutdownExecutorOnClose; 314 this.shutdownScheduledExecutorOnClose = shutdownScheduledExecutorOnClose; 315 316 this.scheduledExecutorService = scheduledExecutorService; 317 318 this.eventListener = eventListener; 319 } 320 321 322 @Override 323 public JWKSet getJWKSet(final JWKSetCacheRefreshEvaluator refreshEvaluator, final long currentTime, final C context) throws KeySourceException { 324 CachedObject<JWKSet> cache = getCachedJWKSet(); 325 if (cache == null) { 326 return loadJWKSetBlocking(JWKSetCacheRefreshEvaluator.noRefresh(), currentTime, context); 327 } 328 329 JWKSet jwkSet = cache.get(); 330 if (refreshEvaluator.requiresRefresh(jwkSet)) { 331 return loadJWKSetBlocking(refreshEvaluator, currentTime, context); 332 } 333 334 if (cache.isExpired(currentTime)) { 335 return loadJWKSetBlocking(JWKSetCacheRefreshEvaluator.referenceComparison(jwkSet), currentTime, context); 336 } 337 338 refreshAheadOfExpiration(cache, false, currentTime, context); 339 340 return cache.get(); 341 } 342 343 344 @Override 345 CachedObject<JWKSet> loadJWKSetNotThreadSafe(final JWKSetCacheRefreshEvaluator refreshEvaluator, final long currentTime, final C context) throws KeySourceException { 346 // Never run by two threads at the same time! 347 CachedObject<JWKSet> cache = super.loadJWKSetNotThreadSafe(refreshEvaluator, currentTime, context); 348 349 if (scheduledExecutorService != null) { 350 scheduleRefreshAheadOfExpiration(cache, currentTime, context); 351 } 352 353 return cache; 354 } 355 356 357 /** 358 * Schedules repeated refresh ahead of cached JWK set expiration. 359 */ 360 void scheduleRefreshAheadOfExpiration(final CachedObject<JWKSet> cache, final long currentTime, final C context) { 361 362 if (scheduledRefreshFuture != null) { 363 scheduledRefreshFuture.cancel(false); 364 } 365 366 // so we want to keep other threads from triggering preemptive refresh 367 // subtracting the refresh timeout should be enough 368 long delay = cache.getExpirationTime() - currentTime - refreshAheadTime - getCacheRefreshTimeout(); 369 if (delay > 0) { 370 final RefreshAheadCachingJWKSetSource<C> that = this; 371 Runnable command = new Runnable() { 372 373 @Override 374 public void run() { 375 try { 376 // so will only refresh if this specific cache entry still is the current one 377 refreshAheadOfExpiration(cache, true, System.currentTimeMillis(), context); 378 } catch (Exception e) { 379 if (eventListener != null) { 380 eventListener.notify(new ScheduledRefreshFailed<C>(that, e, context)); 381 } 382 // ignore 383 } 384 } 385 }; 386 this.scheduledRefreshFuture = scheduledExecutorService.schedule(command, delay, TimeUnit.MILLISECONDS); 387 388 if (eventListener != null) { 389 eventListener.notify(new RefreshScheduledEvent<C>(this, context)); 390 } 391 } else { 392 // cache refresh not scheduled 393 if (eventListener != null) { 394 eventListener.notify(new RefreshNotScheduledEvent<C>(this, context)); 395 } 396 } 397 } 398 399 400 /** 401 * Refreshes the cached JWK set if past the time threshold or refresh 402 * is forced. 403 * 404 * @param cache The current cache. Must not be {@code null}. 405 * @param forceRefresh {@code true} to force refresh. 406 * @param currentTime The current time. 407 */ 408 void refreshAheadOfExpiration(final CachedObject<JWKSet> cache, final boolean forceRefresh, final long currentTime, final C context) { 409 410 if (cache.isExpired(currentTime + refreshAheadTime) || forceRefresh) { 411 412 // cache will expire soon, preemptively update it 413 414 // check if an update is already in progress 415 if (cacheExpiration < cache.getExpirationTime()) { 416 // seems no update is in progress, see if we can get the lock 417 if (lazyLock.tryLock()) { 418 try { 419 lockedRefresh(cache, currentTime, context); 420 } finally { 421 lazyLock.unlock(); 422 } 423 } 424 } 425 } 426 } 427 428 429 /** 430 * Checks if a refresh is in progress and if not triggers one. To be 431 * called by a single thread at a time. 432 * 433 * @param cache The current cache. Must not be {@code null}. 434 * @param currentTime The current time. 435 */ 436 void lockedRefresh(final CachedObject<JWKSet> cache, final long currentTime, final C context) { 437 // check if an update is already in progress (again now that this thread holds the lock) 438 if (cacheExpiration < cache.getExpirationTime()) { 439 440 // still no update is in progress 441 cacheExpiration = cache.getExpirationTime(); 442 443 final RefreshAheadCachingJWKSetSource<C> that = this; 444 445 Runnable runnable = new Runnable() { 446 447 @Override 448 public void run() { 449 try { 450 if (eventListener != null) { 451 eventListener.notify(new ScheduledRefreshInitiatedEvent<>(that, context)); 452 } 453 454 JWKSet jwkSet = RefreshAheadCachingJWKSetSource.this.loadJWKSetBlocking(JWKSetCacheRefreshEvaluator.forceRefresh(), currentTime, context); 455 456 if (eventListener != null) { 457 eventListener.notify(new ScheduledRefreshCompletedEvent<>(that, jwkSet, context)); 458 } 459 460 // so next time this method is invoked, it'll be with the updated cache item expiry time 461 } catch (Throwable e) { 462 // update failed, but another thread can retry 463 cacheExpiration = -1L; 464 // ignore, unable to update 465 // another thread will attempt the same 466 if (eventListener != null) { 467 eventListener.notify(new UnableToRefreshAheadOfExpirationEvent<C>(that, context)); 468 } 469 } 470 } 471 }; 472 // run update in the background 473 executorService.execute(runnable); 474 } 475 } 476 477 478 /** 479 * Returns the executor service running the updates in the background. 480 * 481 * @return The executor service. 482 */ 483 public ExecutorService getExecutorService() { 484 return executorService; 485 } 486 487 488 /** 489 * Returns the scheduled executor service scheduling the updates. 490 * 491 * @return The scheduled executor service. 492 */ 493 public ScheduledExecutorService getScheduledExecutorService() { 494 return scheduledExecutorService; 495 } 496 497 498 ReentrantLock getLazyLock() { 499 return lazyLock; 500 } 501 502 503 /** 504 * Returns the current scheduled refresh future. 505 * 506 * @return The current future, {@code null} if none. 507 */ 508 ScheduledFuture<?> getScheduledRefreshFuture() { 509 return scheduledRefreshFuture; 510 } 511 512 513 @Override 514 public void close() throws IOException { 515 516 ScheduledFuture<?> currentScheduledRefreshFuture = this.scheduledRefreshFuture; // defensive copy 517 if (currentScheduledRefreshFuture != null) { 518 currentScheduledRefreshFuture.cancel(true); 519 } 520 521 super.close(); 522 523 if (shutdownExecutorOnClose) { 524 executorService.shutdownNow(); 525 try { 526 executorService.awaitTermination(getCacheRefreshTimeout(), TimeUnit.MILLISECONDS); 527 } catch (InterruptedException e) { 528 // ignore 529 Thread.currentThread().interrupt(); 530 } 531 } 532 if (scheduledExecutorService != null && shutdownScheduledExecutorOnClose) { 533 scheduledExecutorService.shutdownNow(); 534 try { 535 scheduledExecutorService.awaitTermination(getCacheRefreshTimeout(), TimeUnit.MILLISECONDS); 536 } catch (InterruptedException e) { 537 // ignore 538 Thread.currentThread().interrupt(); 539 } 540 } 541 } 542}