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.util.Objects;
022
023import net.jcip.annotations.ThreadSafe;
024
025import com.nimbusds.jose.KeySourceException;
026import com.nimbusds.jose.jwk.JWKSet;
027import com.nimbusds.jose.proc.SecurityContext;
028import com.nimbusds.jose.util.cache.CachedObject;
029import com.nimbusds.jose.util.events.EventListener;
030
031
032/**
033 * {@linkplain JWKSetSource} with outage tolerance to handle temporary network
034 * issues and endpoint downtime, potentially running into minutes or hours.
035 * Transparently caches the JWK set provided by the wrapped
036 * {@linkplain JWKSetSource}, returning it in case the underlying source throws
037 * a {@linkplain JWKSetUnavailableException}.
038 *
039 * @author Thomas Rørvik Skjølberg
040 * @author Vladimir Dzhuvinov
041 * @version 2022-11-22
042 */
043@ThreadSafe
044public class OutageTolerantJWKSetSource<C extends SecurityContext> extends AbstractCachingJWKSetSource<C> {
045        
046        
047        /**
048         * JWK set source outage event.
049         */
050        public static class OutageEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<OutageTolerantJWKSetSource<C>, C> {
051                
052                private final Exception exception;
053                
054                private final long remainingTime;
055                
056                private OutageEvent(final OutageTolerantJWKSetSource<C> source,
057                                    final Exception exception,
058                                    final long remainingTime,
059                                    final C context) {
060                        super(source, context);
061                        Objects.requireNonNull(exception);
062                        this.exception = exception;
063                        this.remainingTime = remainingTime;
064                }
065                
066                
067                /**
068                 * Returns the exception that caused the retrial.
069                 *
070                 * @return The exception.
071                 */
072                public Exception getException() {
073                        return exception;
074                }
075                
076                
077                /**
078                 * Returns the remaining time until the outage cache expires.
079                 *
080                 * @return The remaining time, in milliseconds.
081                 */
082                public long getRemainingTime() {
083                        return remainingTime;
084                }
085        }
086        
087        
088        private final EventListener<OutageTolerantJWKSetSource<C>, C> eventListener;
089        
090        
091        /**
092         * Creates a new outage tolerant JWK set source.
093         *
094         * @param source        The JWK set source to decorate. Must not be
095         *                      {@code null}.
096         * @param timeToLive    The time to live of the cached JWK set to cover
097         *                      outages, in milliseconds.
098         * @param eventListener The event listener, {@code null} if not
099         *                      specified.
100         */
101        public OutageTolerantJWKSetSource(final JWKSetSource<C> source,
102                                          final long timeToLive,
103                                          final EventListener<OutageTolerantJWKSetSource<C>,C> eventListener) {
104                super(source, timeToLive);
105                this.eventListener = eventListener;
106        }
107
108        
109        @Override
110        public JWKSet getJWKSet(final JWKSetCacheRefreshEvaluator refreshEvaluator, final long currentTime, final C context) throws KeySourceException {
111                try {
112                        // cache if successfully refreshed by the underlying source
113                        JWKSet jwkSet = getSource().getJWKSet(refreshEvaluator, currentTime, context);
114                        cacheJWKSet(jwkSet, currentTime);
115                        return jwkSet;
116                        
117                } catch (JWKSetUnavailableException e) {
118                        // return the previously cached JWT set
119                        CachedObject<JWKSet> cache = getCachedJWKSet();
120                        if (cache != null && cache.isValid(currentTime)) {
121                                long remainingTime = cache.getExpirationTime() - currentTime; // in millis
122                                if (eventListener != null) {
123                                        eventListener.notify(new OutageEvent<>(this, e, remainingTime, context));
124                                }
125                                JWKSet jwkSet = cache.get();
126                                
127                                // There may be in-flight calls waiting to refresh the cache in a parent
128                                // JWKSetSource. Ensure they do not attempt to do so if they passed
129                                // JWKSetCacheEvaluator.referenceComparison(..) or JWKSetCacheEvaluator.noRefresh().
130                                JWKSet jwkSetClone = new JWKSet(jwkSet.getKeys());
131                                if(! refreshEvaluator.requiresRefresh(jwkSetClone)) {
132                                        return jwkSetClone;
133                                }
134                                
135                                // If we made it this far, then JWKSetCacheEvaluator.forceRefresh()
136                                // was passed. If so, propagate the error.
137                        }
138
139                        throw e;
140                }
141        }
142}