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