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
020import net.jcip.annotations.ThreadSafe;
021
022import com.nimbusds.jose.KeySourceException;
023import com.nimbusds.jose.jwk.JWKSet;
024import com.nimbusds.jose.proc.SecurityContext;
025import com.nimbusds.jose.util.events.EventListener;
026
027
028/**
029 * {@linkplain JWKSetSource} that limits the number of requests in a time
030 * period. Intended to guard against frequent, potentially costly, downstream
031 * calls.
032 *
033 * <p>Two invocations per time period are allowed, so that, under normal
034 * operation, there is always one invocation left in case the keys are rotated
035 * and this results in triggering a refresh of the JWK set. The other request
036 * is (sometimes) consumed by background refreshes.
037 *
038 * @author Thomas Rørvik Skjølberg
039 * @author Vladimir Dzhuvinov
040 * @version 2022-11-22
041 */
042@ThreadSafe
043public class RateLimitedJWKSetSource<C extends SecurityContext> extends JWKSetSourceWrapper<C> {
044        
045        /**
046         * Rate limited event.
047         */
048        public static class RateLimitedEvent<C extends SecurityContext> extends AbstractJWKSetSourceEvent<RateLimitedJWKSetSource<C>, C> {
049                
050                private RateLimitedEvent(final RateLimitedJWKSetSource<C> source, final C securityContext) {
051                        super(source, securityContext);
052                }
053        }
054        
055        
056        private final long minTimeInterval;
057        private long nextOpeningTime = -1L;
058        private int counter = 0;
059        private final EventListener<RateLimitedJWKSetSource<C>, C> eventListener;
060
061        
062        /**
063         * Creates a new JWK set source that limits the number of requests.
064         *
065         * @param source          The JWK set source to decorate. Must not be
066         *                        {@code null}.
067         * @param minTimeInterval The minimum allowed time interval between two
068         *                        JWK set retrievals, in milliseconds.
069         * @param eventListener   The event listener, {@code null} if not
070         *                        specified.
071         */
072        public RateLimitedJWKSetSource(final JWKSetSource<C> source,
073                                       final long minTimeInterval,
074                                       final EventListener<RateLimitedJWKSetSource<C>, C> eventListener) {
075                super(source);
076                this.minTimeInterval = minTimeInterval;
077                this.eventListener = eventListener;
078        }
079        
080        
081        @Override
082        public JWKSet getJWKSet(final JWKSetCacheRefreshEvaluator refreshEvaluator, final long currentTime, final C context)
083                throws KeySourceException {
084                
085                // implementation note: this code is not intended to run many parallel threads
086                // for the same instance, thus use of synchronized will not cause congestion
087                
088                boolean rateLimitHit;
089                synchronized (this) {
090                        if (nextOpeningTime <= currentTime) {
091                                nextOpeningTime = currentTime + minTimeInterval;
092                                counter = 1;
093                                rateLimitHit = false;
094                        } else {
095                                rateLimitHit = counter <= 0;
096                                if (! rateLimitHit) {
097                                        counter--;
098                                }
099                        }
100                }
101                if (rateLimitHit) {
102                        if (eventListener != null) {
103                                eventListener.notify(new RateLimitedEvent<>(this, context));
104                        }
105                        throw new RateLimitReachedException();
106                }
107                return getSource().getJWKSet(refreshEvaluator, currentTime, context);
108        }
109        
110        
111        /**
112         * Returns the minimum allowed time interval between two JWK set
113         * retrievals.
114         *
115         * @return The minimum allowed time interval between two JWK set
116         *         retrievals, in milliseconds.
117         */
118        public long getMinTimeInterval() {
119                return minTimeInterval;
120        }
121}