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.net.URL;
022import java.util.Objects;
023import java.util.concurrent.ExecutorService;
024import java.util.concurrent.Executors;
025import java.util.concurrent.ScheduledExecutorService;
026
027import com.nimbusds.jose.proc.SecurityContext;
028import com.nimbusds.jose.util.DefaultResourceRetriever;
029import com.nimbusds.jose.util.ResourceRetriever;
030import com.nimbusds.jose.util.events.EventListener;
031import com.nimbusds.jose.util.health.HealthReportListener;
032
033
034/**
035 * {@linkplain JWKSource} builder.
036 *
037 * <p>Supports wrapping of a JWK set source, typically a URL, with the
038 * following capabilities:
039 *
040 * <ul>
041 *     <li>{@linkplain CachingJWKSetSource caching}
042 *     <li>{@linkplain RefreshAheadCachingJWKSetSource caching with refresh ahead}
043 *     <li>{@linkplain RateLimitedJWKSetSource rate limiting}
044 *     <li>{@linkplain RetryingJWKSetSource retrial}
045 *     <li>{@linkplain JWKSourceWithFailover fail-over}
046 *     <li>{@linkplain JWKSetSourceWithHealthStatusReporting health status reporting}
047 *     <li>{@linkplain OutageTolerantJWKSetSource outage tolerance}
048 * </ul>
049 *
050 * @author Thomas Rørvik Skjølberg
051 * @author Vladimir Dzhuvinov
052 * @version 2025-09-05
053 */
054public class JWKSourceBuilder<C extends SecurityContext> {
055        
056        
057        /**
058         * The default HTTP connect timeout for JWK set retrieval, in
059         * milliseconds. Set to 500 milliseconds.
060         */
061        public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT;
062        
063        
064        /**
065         * The default HTTP read timeout for JWK set retrieval, in
066         * milliseconds. Set to 500 milliseconds.
067         */
068        public static final int DEFAULT_HTTP_READ_TIMEOUT = RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT;
069        
070        
071        /**
072         * The default HTTP entity size limit for JWK set retrieval, in bytes.
073         * Set to 50 KBytes.
074         */
075        public static final int DEFAULT_HTTP_SIZE_LIMIT = RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT;
076        
077        
078        /**
079         * The default time to live of cached JWK sets, in milliseconds. Set to
080         * 5 minutes.
081         */
082        public static final long DEFAULT_CACHE_TIME_TO_LIVE = 5 * 60 * 1000L;
083        
084        
085        /**
086         * The default refresh timeout of cached JWK sets, in milliseconds. Set
087         * to 15 seconds.
088         */
089        public static final long DEFAULT_CACHE_REFRESH_TIMEOUT = 15 * 1000L;
090        
091        
092        /**
093         * The default afresh-ahead time of cached JWK sets, in milliseconds.
094         * Set to 30 seconds.
095         */
096        public static final long DEFAULT_REFRESH_AHEAD_TIME = 30_000L;
097        
098        
099        /**
100         * The default rate limiting minimum allowed time interval between two
101         * JWK set retrievals, in milliseconds.
102         */
103        public static final long DEFAULT_RATE_LIMIT_MIN_INTERVAL = 30_000L;
104        
105        
106        /**
107         * Creates a new JWK source builder using the specified JWK set URL
108         * and {@linkplain DefaultResourceRetriever} with default timeouts.
109         *
110         * @param jwkSetURL The JWK set URL. Must not be {@code null}.
111         */
112        public static <C extends SecurityContext> JWKSourceBuilder<C> create(final URL jwkSetURL) {
113                
114                DefaultResourceRetriever retriever = new DefaultResourceRetriever(
115                        DEFAULT_HTTP_CONNECT_TIMEOUT,
116                        DEFAULT_HTTP_READ_TIMEOUT,
117                        DEFAULT_HTTP_SIZE_LIMIT);
118                
119                JWKSetSource<C> jwkSetSource = new URLBasedJWKSetSource<>(jwkSetURL, retriever);
120                
121                return new JWKSourceBuilder<>(jwkSetSource);
122        }
123        
124        
125        /**
126         * Creates a new JWK source builder using the specified JWK set URL
127         * and resource retriever.
128         *
129         * @param jwkSetURL The JWK set URL. Must not be {@code null}.
130         * @param retriever The resource retriever. Must not be {@code null}.
131         */
132        public static <C extends SecurityContext> JWKSourceBuilder<C> create(final URL jwkSetURL, final ResourceRetriever retriever) {
133                return new JWKSourceBuilder<>(new URLBasedJWKSetSource<C>(jwkSetURL, retriever));
134        }
135        
136        
137        /**
138         * Creates a new JWK source builder wrapping an existing source.
139         *
140         * @param source The JWK source to wrap. Must not be {@code null}.
141         */
142        public static <C extends SecurityContext> JWKSourceBuilder<C> create(final JWKSetSource<C> source) {
143                return new JWKSourceBuilder<>(source);
144        }
145
146        // the wrapped source
147        private final JWKSetSource<C> jwkSetSource;
148
149        // caching
150        private boolean caching = true;
151        private long cacheTimeToLive = DEFAULT_CACHE_TIME_TO_LIVE;
152        private long cacheRefreshTimeout = DEFAULT_CACHE_REFRESH_TIMEOUT;
153        private EventListener<CachingJWKSetSource<C>, C> cachingEventListener;
154
155        private boolean refreshAhead = true;
156        private long refreshAheadTime = DEFAULT_REFRESH_AHEAD_TIME;
157        private boolean refreshAheadScheduled = false;
158        private ExecutorService executorService;
159        private boolean shutdownExecutorOnClose = true;
160        private ScheduledExecutorService scheduledExecutorService;
161        private boolean shutdownScheduledExecutorOnClose = true;
162
163        // rate limiting (retry on network error will not count against this)
164        private boolean rateLimited = true;
165        private long minTimeInterval = DEFAULT_RATE_LIMIT_MIN_INTERVAL;
166        private EventListener<RateLimitedJWKSetSource<C>, C> rateLimitedEventListener;
167
168        // retrying
169        private boolean retrying = false;
170        private EventListener<RetryingJWKSetSource<C>, C> retryingEventListener;
171
172        // outage
173        private boolean outageTolerant = false;
174        private long outageCacheTimeToLive = -1L;
175        private EventListener<OutageTolerantJWKSetSource<C>, C> outageEventListener;
176
177        // health status reporting
178        private HealthReportListener<JWKSetSourceWithHealthStatusReporting<C>, C> healthReportListener;
179
180        // failover
181        protected JWKSource<C> failover;
182        
183
184        /**
185         * Creates a new JWK set source.
186         *
187         * @param jwkSetSource The JWK set source to wrap. Must not be
188         *                     {@code null}.
189         */
190        private JWKSourceBuilder(final JWKSetSource<C> jwkSetSource) {
191                Objects.requireNonNull(jwkSetSource);
192                this.jwkSetSource = jwkSetSource;
193        }
194
195        
196        /**
197         * Toggles caching of the JWK set.
198         *
199         * @param enable {@code true} to cache the JWK set.
200         *
201         * @return This builder.
202         */
203        public JWKSourceBuilder<C> cache(final boolean enable) {
204                this.caching = enable;
205                return this;
206        }
207
208
209        /**
210         * Enables caching of the retrieved JWK set.
211         * 
212         * @param timeToLive          The time to live of the cached JWK set,
213         *                            in milliseconds.
214         * @param cacheRefreshTimeout The cache refresh timeout, in
215         *                            milliseconds.
216         *
217         * @return This builder.
218         */
219        public JWKSourceBuilder<C> cache(final long timeToLive, final long cacheRefreshTimeout) {
220                this.caching = true;
221                this.cacheTimeToLive = timeToLive;
222                this.cacheRefreshTimeout = cacheRefreshTimeout;
223                return this;
224        }
225
226
227        /**
228         * Enables caching of the retrieved JWK set.
229         *
230         * @param timeToLive          The time to live of the cached JWK set,
231         *                            in milliseconds.
232         * @param cacheRefreshTimeout The cache refresh timeout, in
233         *                            milliseconds.
234         * @param eventListener       The event listener, {@code null} if not
235         *                            specified.
236         *
237         * @return This builder.
238         */
239        public JWKSourceBuilder<C> cache(final long timeToLive,
240                                         final long cacheRefreshTimeout,
241                                         final EventListener<CachingJWKSetSource<C>, C> eventListener) {
242                this.caching = true;
243                this.cacheTimeToLive = timeToLive;
244                this.cacheRefreshTimeout = cacheRefreshTimeout;
245                this.cachingEventListener = eventListener;
246                return this;
247        }
248
249
250        /**
251         * Enables caching of the JWK set forever (no expiration).
252         *
253         * @return This builder.
254         */
255        public JWKSourceBuilder<C> cacheForever() {
256                this.caching = true;
257                this.cacheTimeToLive = Long.MAX_VALUE;
258                this.refreshAhead = false; // refresh ahead not necessary
259                return this;
260        }
261        
262        
263        /**
264         * Toggles refresh-ahead caching of the JWK set.
265         *
266         * @param enable {@code true} to enable refresh-ahead caching of the
267         *               JWK set.
268         *
269         * @return This builder.
270         */
271        public JWKSourceBuilder<C> refreshAheadCache(final boolean enable) {
272                if (enable) {
273                        this.caching = true;
274                }
275                this.refreshAhead = enable;
276                return this;
277        }
278        
279        
280        /**
281         * Enables refresh-ahead caching of the JWK set.
282         *
283         * @param refreshAheadTime The refresh ahead time, in milliseconds.
284         * @param scheduled        {@code true} to refresh in a scheduled
285         *                         manner, regardless of requests.
286         *
287         * @return This builder.
288         */
289        public JWKSourceBuilder<C> refreshAheadCache(final long refreshAheadTime, final boolean scheduled) {
290                this.caching = true;
291                this.refreshAhead = true;
292                this.refreshAheadTime = refreshAheadTime;
293                this.refreshAheadScheduled = scheduled;
294                return this;
295        }
296        
297        
298        /**
299         * Enables refresh-ahead caching of the JWK set.
300         *
301         * @param refreshAheadTime The refresh ahead time, in milliseconds.
302         * @param scheduled        {@code true} to refresh in a scheduled
303         *                         manner, regardless of requests.
304         * @param eventListener    The event listener, {@code null} if not
305         *                         specified.
306         *
307         * @return This builder.
308         */
309        public JWKSourceBuilder<C> refreshAheadCache(final long refreshAheadTime,
310                                                     final boolean scheduled,
311                                                     final EventListener<CachingJWKSetSource<C>, C> eventListener) {
312                this.caching = true;
313                this.refreshAhead = true;
314                this.refreshAheadTime = refreshAheadTime;
315                this.refreshAheadScheduled = scheduled;
316                this.cachingEventListener = eventListener;
317                return this;
318        }
319
320
321        /**
322         * Enables refresh-ahead caching of the JWK set.
323         *
324         * @param refreshAheadTime                 The refresh ahead time, in
325         *                                         milliseconds.
326         * @param eventListener                    The event listener,
327         *                                         {@code null} if not
328         *                                         specified.
329         * @param executorService                  The executor service to use
330         *                                         for the cache refresh.
331         * @param shutdownExecutorOnClose          If {@code true} the executor
332         *                                         service will be shut down
333         *                                         upon closing the source.
334         * @param scheduledExecutorService         The {@link ScheduledExecutorService}
335         *                                         to schedule the updates in
336         *                                         the background. If
337         *                                         {@code null} no updates will
338         *                                         be scheduled
339         * @param shutdownScheduledExecutorOnClose If {@code true} then the {@code ScheduledExecutorService}
340         *                                         will be shut down upon closing the source.
341         *
342         * @return This builder.
343         */
344        public JWKSourceBuilder<C> refreshAheadCache(final long refreshAheadTime,
345                                                     final EventListener<CachingJWKSetSource<C>, C> eventListener,
346                                                     final ExecutorService executorService,
347                                                     final boolean shutdownExecutorOnClose,
348                                                     final ScheduledExecutorService scheduledExecutorService,
349                                                     final boolean shutdownScheduledExecutorOnClose){
350                this.caching = true;
351                this.refreshAhead = true;
352                this.refreshAheadTime = refreshAheadTime;
353                this.refreshAheadScheduled = scheduledExecutorService != null;
354                this.cachingEventListener = eventListener;
355                this.executorService = executorService;
356                this.shutdownExecutorOnClose = shutdownExecutorOnClose;
357                this.scheduledExecutorService = scheduledExecutorService;
358                this.shutdownScheduledExecutorOnClose = shutdownScheduledExecutorOnClose;
359                return this;
360        }
361
362
363        /**
364         * Toggles rate limiting of the JWK set retrieval.
365         *
366         * @param enable {@code true} to rate limit the JWK set retrieval.
367         *                           
368         * @return This builder.
369         */
370        public JWKSourceBuilder<C> rateLimited(final boolean enable) {
371                this.rateLimited = enable;
372                return this;
373        }
374
375        
376        /**
377         * Enables rate limiting of the JWK set retrieval.
378         *
379         * @param minTimeInterval The minimum allowed time interval between two
380         *                        JWK set retrievals, in milliseconds.
381         *
382         * @return This builder.
383         */
384        public JWKSourceBuilder<C> rateLimited(final long minTimeInterval) {
385                this.rateLimited = true;
386                this.minTimeInterval = minTimeInterval;
387                return this;
388        }
389
390        
391        /**
392         * Enables rate limiting of the JWK set retrieval.
393         *
394         * @param minTimeInterval The minimum allowed time interval between two
395         *                        JWK set retrievals, in milliseconds.
396         * @param eventListener   The event listener, {@code null} if not
397         *                        specified.
398         *
399         * @return This builder.
400         */
401        public JWKSourceBuilder<C> rateLimited(final long minTimeInterval,
402                                               final EventListener<RateLimitedJWKSetSource<C>, C> eventListener) {
403                this.rateLimited = true;
404                this.minTimeInterval = minTimeInterval;
405                this.rateLimitedEventListener = eventListener;
406                return this;
407        }
408        
409        
410        /**
411         * Sets a failover JWK source.
412         *
413         * @param failover The failover JWK source, {@code null} if none.
414         *
415         * @return This builder.
416         */
417        public JWKSourceBuilder<C> failover(final JWKSource<C> failover) {
418                this.failover = failover;
419                return this;
420        }
421        
422        
423        /**
424         * Enables single retrial to retrieve the JWK set to work around
425         * transient network issues.
426         * 
427         * @param enable {@code true} to enable single retrial.
428         *
429         * @return This builder.
430         */
431        public JWKSourceBuilder<C> retrying(final boolean enable) {
432                this.retrying = enable;
433                return this;
434        }
435        
436        
437        /**
438         * Enables single retrial to retrieve the JWK set to work around
439         * transient network issues.
440         *
441         * @param eventListener The event listener, {@code null} if not
442         *                      specified.
443         *
444         * @return This builder.
445         */
446        public JWKSourceBuilder<C> retrying(final EventListener<RetryingJWKSetSource<C>, C> eventListener) {
447                this.retrying = true;
448                this.retryingEventListener = eventListener;
449                return this;
450        }
451
452        
453        /**
454         * Sets a health report listener.
455         *
456         * @param listener The health report listener, {@code null} if not
457         *                 specified.
458         *
459         * @return This builder.
460         */
461        public JWKSourceBuilder<C> healthReporting(final HealthReportListener<JWKSetSourceWithHealthStatusReporting<C>, C> listener) {
462                this.healthReportListener = listener;
463                return this;
464        }
465        
466        
467        /**
468         * Toggles outage tolerance by serving a cached JWK set in case of
469         * outage.
470         *
471         * @param enable {@code true} to enable the outage cache.
472         *
473         * @return This builder.
474         */
475        public JWKSourceBuilder<C> outageTolerant(final boolean enable) {
476                this.outageTolerant = enable;
477                return this;
478        }
479
480        
481        /**
482         * Enables outage tolerance by serving a non-expiring cached JWK set in
483         * case of outage.
484         *
485         * @return This builder.
486         */
487        public JWKSourceBuilder<C> outageTolerantForever() {
488                this.outageTolerant = true;
489                this.outageCacheTimeToLive = Long.MAX_VALUE;
490                return this;
491        }
492        
493        
494        /**
495         * Enables outage tolerance by serving a non-expiring cached JWK set in
496         * case of outage.
497         *
498         * @param timeToLive The time to live of the cached JWK set to cover
499         *                   outages, in milliseconds.
500         *
501         * @return This builder.
502         */
503        public JWKSourceBuilder<C> outageTolerant(final long timeToLive) {
504                this.outageTolerant = true;
505                this.outageCacheTimeToLive = timeToLive;
506                return this;
507        }
508        
509        
510        /**
511         * Enables outage tolerance by serving a non-expiring cached JWK set in
512         * case of outage.
513         *
514         * @param timeToLive    The time to live of the cached JWK set to cover
515         *                      outages, in milliseconds.
516         * @param eventListener The event listener, {@code null} if not
517         *                      specified.
518         *
519         * @return This builder.
520         */
521        public JWKSourceBuilder<C> outageTolerant(final long timeToLive,
522                                                  final EventListener<OutageTolerantJWKSetSource<C>, C> eventListener) {
523                this.outageTolerant = true;
524                this.outageCacheTimeToLive = timeToLive;
525                this.outageEventListener = eventListener;
526                return this;
527        }
528
529        
530        /**
531         * Builds the final {@link JWKSource}.
532         *
533         * @return The final {@link JWKSource}.
534         */
535        public JWKSource<C> build() {
536                
537                if (! caching && rateLimited) {
538                        throw new IllegalStateException("Rate limiting requires caching");
539                } else if (! caching && refreshAhead) {
540                        throw new IllegalStateException("Refresh-ahead caching requires general caching");
541                }
542
543                if (caching && rateLimited && cacheTimeToLive <= minTimeInterval) {
544                        throw new IllegalStateException("The rate limiting min time interval between requests must be less than the cache time-to-live");
545                }
546                
547                if (caching && outageTolerant && cacheTimeToLive == Long.MAX_VALUE && outageCacheTimeToLive == Long.MAX_VALUE) {
548                        // TODO consider adjusting instead of exception
549                        throw new IllegalStateException("Outage tolerance not necessary with a non-expiring cache");
550                }
551
552                if (caching && refreshAhead && cacheTimeToLive == Long.MAX_VALUE) {
553                        // TODO consider adjusting instead of exception
554                        throw new IllegalStateException("Refresh-ahead caching not necessary with a non-expiring cache");
555                }
556                
557                JWKSetSource<C> source = jwkSetSource;
558
559                if (retrying) {
560                        source = new RetryingJWKSetSource<>(source, retryingEventListener);
561                }
562                
563                if (outageTolerant) {
564                        if (outageCacheTimeToLive == -1L) {
565                                if (caching) {
566                                        outageCacheTimeToLive = cacheTimeToLive * 10;
567                                } else {
568                                        outageCacheTimeToLive = DEFAULT_CACHE_TIME_TO_LIVE * 10;
569                                }
570                        }
571                        source = new OutageTolerantJWKSetSource<>(source, outageCacheTimeToLive, outageEventListener);
572                }
573
574                if (healthReportListener != null) {
575                        source = new JWKSetSourceWithHealthStatusReporting<>(source, healthReportListener);
576                }
577
578                if (rateLimited) {
579                        source = new RateLimitedJWKSetSource<>(source, minTimeInterval, rateLimitedEventListener);
580                }
581                
582                if (refreshAhead) {
583                        if (refreshAheadScheduled){
584                                if (scheduledExecutorService == null){
585                                        scheduledExecutorService = RefreshAheadCachingJWKSetSource.createDefaultScheduledExecutorService();
586                                }
587                        }
588                        if (executorService == null){
589                                executorService = RefreshAheadCachingJWKSetSource.createDefaultExecutorService();
590                        }
591                        source = new RefreshAheadCachingJWKSetSource<>(source, cacheTimeToLive, cacheRefreshTimeout, refreshAheadTime, executorService, shutdownExecutorOnClose, cachingEventListener, scheduledExecutorService, shutdownScheduledExecutorOnClose);
592                } else if (caching) {
593                        source = new CachingJWKSetSource<>(source, cacheTimeToLive, cacheRefreshTimeout, cachingEventListener);
594                }
595
596                JWKSource<C> jwkSource = new JWKSetBasedJWKSource<>(source);
597                if (failover != null) {
598                        return new JWKSourceWithFailover<>(jwkSource, failover);
599                }
600                return jwkSource;
601        }
602}