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}