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}