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}