From 5985016accedf1020c4bde999cd3e952b693d1a6 Mon Sep 17 00:00:00 2001 From: yuriyz Date: Fri, 19 Dec 2025 20:08:11 +0200 Subject: [PATCH 1/8] feat(jans-auth-server): Add configurable rate limiting for authentication endpoints to prevent brute-force attacks #12664 Signed-off-by: YuriyZ Signed-off-by: yuriyz --- .../jans/as/server/rate/RateLimitFilter.java | 43 +----------------- .../jans/as/server/rate/RateLimitService.java | 44 +++++++++++++++++++ .../as/server/rate/RateLimitServiceTest.java | 4 ++ 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitFilter.java b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitFilter.java index eb265eec841..01f35d51df4 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitFilter.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitFilter.java @@ -1,9 +1,6 @@ package io.jans.as.server.rate; -import io.jans.as.client.RegisterRequest; -import io.jans.as.model.common.FeatureFlagType; import io.jans.as.model.config.Constants; -import io.jans.as.model.error.ErrorResponseFactory; import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.servlet.*; @@ -12,14 +9,11 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.core.Response; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import java.io.IOException; import java.io.PrintWriter; -import java.util.List; /** * @author Yuriy Z @@ -40,8 +34,6 @@ public class RateLimitFilter implements Filter { private Logger log; @Inject private RateLimitService rateLimitService; - @Inject - private ErrorResponseFactory errorResponseFactory; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { @@ -49,7 +41,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha HttpServletResponse httpResponse = (HttpServletResponse) response; try { - httpRequest = validateRateLimit(httpRequest); + httpRequest = rateLimitService.validateRateLimit(httpRequest); chain.doFilter(httpRequest, httpResponse); } catch (RateLimitedException e) { sendTooManyRequestsError(httpResponse); @@ -62,39 +54,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } } - private HttpServletRequest validateRateLimit(HttpServletRequest httpRequest) throws RateLimitedException, IOException { - // if rate_limit flag is disabled immediately return - if (!errorResponseFactory.isFeatureFlagEnabled(FeatureFlagType.RATE_LIMIT)){ - return httpRequest; - } - - final String requestUrl = httpRequest.getRequestURL().toString(); - - boolean isRegisterEndpoint = requestUrl.endsWith("/register"); - - if (isRegisterEndpoint) { - CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest); - final RegisterRequest registerRequest = rateLimitService.parseRegisterRequest(cachedRequest.getCachedBodyAsString()); - String key = "no_key"; - if (registerRequest != null) { - final String ssa = registerRequest.getSoftwareStatement(); - final List redirectUris = registerRequest.getRedirectUris(); - - if (StringUtils.isNotBlank(ssa)) { - // hash ssa to save memory - key = DigestUtils.sha256Hex(ssa); - } else if (CollectionUtils.isNotEmpty(redirectUris) && StringUtils.isNotBlank(redirectUris.get(0))) { - key = redirectUris.get(0); - } - } - - rateLimitService.validateRateLimitForRegister(key); - return cachedRequest; - } - - return httpRequest; - } - private void sendTooManyRequestsError(HttpServletResponse servletResponse) { sendResponse(servletResponse, Response.Status.TOO_MANY_REQUESTS, TOO_MANY_REQUESTS_JSON_ERROR); } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitService.java index 410a94ffa04..501e68413cb 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitService.java @@ -5,14 +5,22 @@ import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket; import io.jans.as.client.RegisterRequest; +import io.jans.as.model.common.FeatureFlagType; import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.error.ErrorResponseFactory; import io.jans.as.server.register.ws.rs.RegisterService; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.json.JSONObject; import org.slf4j.Logger; +import java.io.IOException; import java.time.Duration; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -39,6 +47,42 @@ public class RateLimitService { @Inject private RegisterService registerService; + @Inject + private ErrorResponseFactory errorResponseFactory; + + public HttpServletRequest validateRateLimit(HttpServletRequest httpRequest) throws RateLimitedException, IOException { + // if rate_limit flag is disabled immediately return + if (!errorResponseFactory.isFeatureFlagEnabled(FeatureFlagType.RATE_LIMIT)){ + return httpRequest; + } + + final String requestUrl = httpRequest.getRequestURL().toString(); + + boolean isRegisterEndpoint = requestUrl.endsWith("/register"); + + if (isRegisterEndpoint) { + CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest); + final RegisterRequest registerRequest = parseRegisterRequest(cachedRequest.getCachedBodyAsString()); + String key = "no_key"; + if (registerRequest != null) { + final String ssa = registerRequest.getSoftwareStatement(); + final List redirectUris = registerRequest.getRedirectUris(); + + if (StringUtils.isNotBlank(ssa)) { + // hash ssa to save memory + key = DigestUtils.sha256Hex(ssa); + } else if (CollectionUtils.isNotEmpty(redirectUris) && StringUtils.isNotBlank(redirectUris.get(0))) { + key = redirectUris.get(0); + } + } + + validateRateLimitForRegister(key); + return cachedRequest; + } + + return httpRequest; + } + public void validateRateLimitForRegister(String key) throws RateLimitedException { int requestLimit = getRequestLimit(appConfiguration.getRateLimitRegistrationRequestCount()); int periodLimit = getPeriodLimit(appConfiguration.getRateLimitRegistrationPeriodInSeconds()); diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/rate/RateLimitServiceTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/rate/RateLimitServiceTest.java index ceff34aeb88..a2782d1ca22 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/rate/RateLimitServiceTest.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/rate/RateLimitServiceTest.java @@ -1,6 +1,7 @@ package io.jans.as.server.rate; import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.error.ErrorResponseFactory; import io.jans.as.server.register.ws.rs.RegisterService; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -32,6 +33,9 @@ public class RateLimitServiceTest { @Mock private RegisterService registerService; + @Mock + private ErrorResponseFactory errorResponseFactory; + @Test public void validateRateLimitForRegister_forSingleCall_shouldPassSuccessfully() throws RateLimitedException { rateLimitService.validateRateLimitForRegister("some_ssa"); From a81a0f87a79b1b6bb619f6e03cd6809b9fb60a22 Mon Sep 17 00:00:00 2001 From: yuriyz Date: Mon, 22 Dec 2025 14:53:33 +0200 Subject: [PATCH 2/8] added rate limit configuration Signed-off-by: YuriyZ Signed-off-by: yuriyz --- .../model/configuration/AppConfiguration.java | 12 ++ .../configuration/rate/KeyExtractor.java | 82 ++++++++++ .../model/configuration/rate/KeySource.java | 45 ++++++ .../configuration/rate/RateLimitConfig.java | 56 +++++++ .../configuration/rate/RateLimitRule.java | 144 ++++++++++++++++++ .../jans/as/server/rate/RateLimitService.java | 8 + 6 files changed, 347 insertions(+) create mode 100644 jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeyExtractor.java create mode 100644 jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeySource.java create mode 100644 jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitConfig.java create mode 100644 jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitRule.java diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java index 445c5cfb80e..7c5773cb325 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java @@ -11,6 +11,7 @@ import com.google.common.collect.Lists; import io.jans.agama.model.EngineConfig; import io.jans.as.model.common.*; +import io.jans.as.model.configuration.rate.RateLimitConfig; import io.jans.as.model.crypto.signature.SignatureAlgorithm; import io.jans.as.model.error.ErrorHandlingMethod; import io.jans.as.model.jwk.KeySelectionStrategy; @@ -970,6 +971,9 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "DCR SSA Validation configurations used to perform validation of SSA or DCR. Only needed if softwareStatementValidationType=builtin") private List dcrSsaValidationConfigs; + @DocProperty(description = "Rate Limit Configuration") + private RateLimitConfig rateLimitConfiguration; + @DocProperty(description = "SSA Configuration") private SsaConfiguration ssaConfiguration; @@ -3803,6 +3807,14 @@ public void setSsaConfiguration(SsaConfiguration ssaConfiguration) { this.ssaConfiguration = ssaConfiguration; } + public RateLimitConfig getRateLimitConfiguration() { + return rateLimitConfiguration; + } + + public void setRateLimitConfiguration(RateLimitConfig rateLimitConfiguration) { + this.rateLimitConfiguration = rateLimitConfiguration; + } + public Boolean getAuthorizationChallengeShouldGenerateSession() { if (authorizationChallengeShouldGenerateSession == null) authorizationChallengeShouldGenerateSession = false; return authorizationChallengeShouldGenerateSession; diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeyExtractor.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeyExtractor.java new file mode 100644 index 00000000000..78fba423b46 --- /dev/null +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeyExtractor.java @@ -0,0 +1,82 @@ +package io.jans.as.model.configuration.rate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class KeyExtractor { + + private KeySource source; + private List parameterNames = new ArrayList<>(); + + public KeyExtractor() { + } + + @JsonCreator + public KeyExtractor(@JsonProperty("source") KeySource source, @JsonProperty("parameterNames") List parameterNames) { + setSource(source); + setParameterNames(parameterNames); + } + + @JsonProperty("source") + public KeySource getSource() { + return source; + } + + @JsonProperty("source") + public void setSource(KeySource source) { + this.source = source; + } + + @JsonProperty("parameterNames") + public List getParameterNames() { + return parameterNames == null ? Collections.emptyList() : Collections.unmodifiableList(parameterNames); + } + + @JsonProperty("parameterNames") + public void setParameterNames(List parameterNames) { + // Defensive copy + filter null/blank + List safe = new ArrayList<>(); + if (parameterNames != null) { + for (String p : parameterNames) { + if (p == null) continue; + String v = p.trim(); + if (!v.isEmpty()) safe.add(v); + } + } + this.parameterNames = safe; + } + + public boolean isWellFormed() { + return source != null && !getParameterNames().isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof KeyExtractor)) return false; + KeyExtractor that = (KeyExtractor) o; + return source == that.source && Objects.equals(getParameterNames(), that.getParameterNames()); + } + + @Override + public int hashCode() { + return Objects.hash(source, getParameterNames()); + } + + @Override + public String toString() { + return "KeyExtractor{" + + "source=" + source + + ", parameterNames=" + getParameterNames() + + '}'; + } +} diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeySource.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeySource.java new file mode 100644 index 00000000000..9261e127b73 --- /dev/null +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeySource.java @@ -0,0 +1,45 @@ +package io.jans.as.model.configuration.rate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Where to extract the key from. + *

+ * Defensive behavior: + * - Unknown values deserialize to {@link #UNKNOWN} instead of failing. + * - Serialization uses the json value (lower-case). + */ +public enum KeySource { + BODY("body"), + HEADER("header"), + QUERY("query"), + PATH("path"), + UNKNOWN("unknown"); + + private final String jsonValue; + + KeySource(String jsonValue) { + this.jsonValue = jsonValue; + } + + @JsonCreator + public static KeySource fromJson(String value) { + if (value == null) return null; // preserve null if field absent + String v = value.trim(); + if (v.isEmpty()) return null; + + for (KeySource s : values()) { + if (s.jsonValue.equalsIgnoreCase(v)) { + return s; + } + } + // Defensive: don't hard-fail on new/typo values + return UNKNOWN; + } + + @JsonValue + public String toJson() { + return jsonValue; + } +} diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitConfig.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitConfig.java new file mode 100644 index 00000000000..98052d34751 --- /dev/null +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitConfig.java @@ -0,0 +1,56 @@ +package io.jans.as.model.configuration.rate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RateLimitConfig { + + private List rateLimitRules = new ArrayList<>(); + + public RateLimitConfig() { + } + + @JsonCreator + public RateLimitConfig(@JsonProperty("rateLimitRules") List rateLimitRules) { + setRateLimitRules(rateLimitRules); + } + + @JsonProperty("rateLimitRules") + public List getRateLimitRules() { + return rateLimitRules == null ? Collections.emptyList() : Collections.unmodifiableList(rateLimitRules); + } + + @JsonProperty("rateLimitRules") + public void setRateLimitRules(List rateLimitRules) { + this.rateLimitRules = (rateLimitRules == null) ? new ArrayList<>() : new ArrayList<>(rateLimitRules); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RateLimitConfig)) return false; + RateLimitConfig that = (RateLimitConfig) o; + return Objects.equals(getRateLimitRules(), that.getRateLimitRules()); + } + + @Override + public int hashCode() { + return Objects.hash(getRateLimitRules()); + } + + @Override + public String toString() { + return "RateLimitConfig{" + + "rateLimitRules=" + getRateLimitRules() + + '}'; + } +} diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitRule.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitRule.java new file mode 100644 index 00000000000..fb140c15396 --- /dev/null +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitRule.java @@ -0,0 +1,144 @@ +package io.jans.as.model.configuration.rate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RateLimitRule { + + private String endpoint; + private List methods = new ArrayList<>(); + private Integer requestCount; + private Integer periodInSeconds; + private List keyExtractors = new ArrayList<>(); + + public RateLimitRule() { + } + + @JsonCreator + public RateLimitRule( + @JsonProperty("endpoint") String endpoint, + @JsonProperty("methods") List methods, + @JsonProperty("requestCount") Integer requestCount, + @JsonProperty("periodInSeconds") Integer periodInSeconds, + @JsonProperty("keyExtractors") List keyExtractors + ) { + setEndpoint(endpoint); + setMethods(methods); + setRequestCount(requestCount); + setPeriodInSeconds(periodInSeconds); + setKeyExtractors(keyExtractors); + } + + @JsonProperty("endpoint") + public String getEndpoint() { + return endpoint; + } + + @JsonProperty("endpoint") + public void setEndpoint(String endpoint) { + this.endpoint = (endpoint == null || endpoint.trim().isEmpty()) ? null : endpoint.trim(); + } + + @JsonProperty("methods") + public List getMethods() { + return methods == null ? Collections.emptyList() : Collections.unmodifiableList(methods); + } + + @JsonProperty("methods") + public void setMethods(List methods) { + // Defensive copy + filter null/blank + List safe = new ArrayList<>(); + if (methods != null) { + for (String m : methods) { + if (m == null) continue; + String v = m.trim(); + if (!v.isEmpty()) safe.add(v); + } + } + this.methods = safe; + } + + @JsonProperty("requestCount") + public Integer getRequestCount() { + return requestCount; + } + + @JsonProperty("requestCount") + public void setRequestCount(Integer requestCount) { + this.requestCount = (requestCount != null && requestCount > 0) ? requestCount : null; + } + + @JsonProperty("periodInSeconds") + public Integer getPeriodInSeconds() { + return periodInSeconds; + } + + @JsonProperty("periodInSeconds") + public void setPeriodInSeconds(Integer periodInSeconds) { + this.periodInSeconds = (periodInSeconds != null && periodInSeconds > 0) ? periodInSeconds : null; + } + + @JsonProperty("keyExtractors") + public List getKeyExtractors() { + return keyExtractors == null ? Collections.emptyList() : Collections.unmodifiableList(keyExtractors); + } + + @JsonProperty("keyExtractors") + public void setKeyExtractors(List keyExtractors) { + List safe = new ArrayList<>(); + if (keyExtractors != null) { + for (KeyExtractor ke : keyExtractors) { + if (ke != null) safe.add(ke); + } + } + this.keyExtractors = safe; + } + + /** + * Non-throwing helper for validation-style checks in business logic. + */ + public boolean isWellFormed() { + return endpoint != null + && !getMethods().isEmpty() + && requestCount != null + && periodInSeconds != null + && !getKeyExtractors().isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RateLimitRule)) return false; + RateLimitRule that = (RateLimitRule) o; + return Objects.equals(endpoint, that.endpoint) + && Objects.equals(getMethods(), that.getMethods()) + && Objects.equals(requestCount, that.requestCount) + && Objects.equals(periodInSeconds, that.periodInSeconds) + && Objects.equals(getKeyExtractors(), that.getKeyExtractors()); + } + + @Override + public int hashCode() { + return Objects.hash(endpoint, getMethods(), requestCount, periodInSeconds, getKeyExtractors()); + } + + @Override + public String toString() { + return "RateLimitRule{" + + "endpoint='" + endpoint + '\'' + + ", methods=" + getMethods() + + ", requestCount=" + requestCount + + ", periodInSeconds=" + periodInSeconds + + ", keyExtractors=" + getKeyExtractors() + + '}'; + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitService.java index 501e68413cb..3c4316c67cf 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitService.java @@ -7,6 +7,7 @@ import io.jans.as.client.RegisterRequest; import io.jans.as.model.common.FeatureFlagType; import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.configuration.rate.RateLimitConfig; import io.jans.as.model.error.ErrorResponseFactory; import io.jans.as.server.register.ws.rs.RegisterService; import jakarta.enterprise.context.ApplicationScoped; @@ -56,6 +57,13 @@ public HttpServletRequest validateRateLimit(HttpServletRequest httpRequest) thro return httpRequest; } + RateLimitConfig rateLimitConfiguration = appConfiguration.getRateLimitConfiguration(); + + // no rate limit configuration -> return + if (rateLimitConfiguration == null) { + return httpRequest; + } + final String requestUrl = httpRequest.getRequestURL().toString(); boolean isRegisterEndpoint = requestUrl.endsWith("/register"); From 1f70e3d32c7c8c229a3afe09f8db8abdb1ef6d92 Mon Sep 17 00:00:00 2001 From: yuriyz Date: Mon, 29 Dec 2025 14:01:36 +0200 Subject: [PATCH 3/8] Removed redundant rate limiting configuration. We will use requestCound and period from rate limiting rules. Signed-off-by: YuriyZ Signed-off-by: yuriyz --- .../model/configuration/AppConfiguration.java | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java index 7c5773cb325..8d6eb165cb1 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java @@ -747,12 +747,6 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "Authorization challenge session lifetime in seconds") private Integer authorizationChallengeSessionLifetimeInSeconds; - @DocProperty(description = "Request count limit - for /register endpoint (Rate Limit)") - private Integer rateLimitRegistrationRequestCount; - - @DocProperty(description = "Period in seconds limit - for /register endpoint (Rate Limit)") - private Integer rateLimitRegistrationPeriodInSeconds; - // Token Exchange @DocProperty(description = "", defaultValue = "false") private Boolean rotateDeviceSecret = false; @@ -1119,24 +1113,6 @@ public void setReturnDeviceSecretFromAuthzEndpoint(Boolean returnDeviceSecretFro this.returnDeviceSecretFromAuthzEndpoint = returnDeviceSecretFromAuthzEndpoint; } - public Integer getRateLimitRegistrationRequestCount() { - return rateLimitRegistrationRequestCount; - } - - public AppConfiguration setRateLimitRegistrationRequestCount(Integer rateLimitRegistrationRequestCount) { - this.rateLimitRegistrationRequestCount = rateLimitRegistrationRequestCount; - return this; - } - - public Integer getRateLimitRegistrationPeriodInSeconds() { - return rateLimitRegistrationPeriodInSeconds; - } - - public AppConfiguration setRateLimitRegistrationPeriodInSeconds(Integer rateLimitRegistrationPeriodInSeconds) { - this.rateLimitRegistrationPeriodInSeconds = rateLimitRegistrationPeriodInSeconds; - return this; - } - public Integer getAuthorizationChallengeSessionLifetimeInSeconds() { if (authorizationChallengeSessionLifetimeInSeconds == null) { authorizationChallengeSessionLifetimeInSeconds = DEFAULT_AUTHORIZATION_CHALLENGE_SESSION_LIFETIME; From f7cdabb90f469043419311fa9f6682c768d3ff67 Mon Sep 17 00:00:00 2001 From: yuriyz Date: Fri, 2 Jan 2026 12:21:31 +0200 Subject: [PATCH 4/8] Limited cached body size to 1MB to avoid Out Of Memory during rate limiting Signed-off-by: YuriyZ Signed-off-by: yuriyz --- .../server/rate/CachedBodyHttpServletRequest.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/rate/CachedBodyHttpServletRequest.java b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/CachedBodyHttpServletRequest.java index 49fa97e1f72..00430ae0ca0 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/rate/CachedBodyHttpServletRequest.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/CachedBodyHttpServletRequest.java @@ -13,14 +13,25 @@ */ public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { + public static final int MAX_BODY_SIZE = 1024 * 1024; // 1 MB limit + private final byte[] cachedBody; public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { super(request); + int contentLength = request.getContentLength(); + if (contentLength > MAX_BODY_SIZE) { + throw new IOException("Request body exceeds maximum allowed size: " + MAX_BODY_SIZE); + } + // Read the entire body and cache it. InputStream is = request.getInputStream(); - this.cachedBody = is.readAllBytes(); + this.cachedBody = is.readNBytes(MAX_BODY_SIZE + 1); + + if (this.cachedBody.length > MAX_BODY_SIZE) { + throw new IOException("Actual body size exceeded 1MB limit."); + } } public byte[] getCachedBody() { From 4f3d04ec2acef253f3640112227a917a316d00e7 Mon Sep 17 00:00:00 2001 From: yuriyz Date: Mon, 5 Jan 2026 14:04:31 +0200 Subject: [PATCH 5/8] Added specific flag for rate limit logging (because it's very verbose) Signed-off-by: YuriyZ Signed-off-by: yuriyz --- .../jans/as/model/configuration/rate/KeySource.java | 1 - .../as/model/configuration/rate/RateLimitConfig.java | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeySource.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeySource.java index 9261e127b73..747d472d719 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeySource.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/KeySource.java @@ -14,7 +14,6 @@ public enum KeySource { BODY("body"), HEADER("header"), QUERY("query"), - PATH("path"), UNKNOWN("unknown"); private final String jsonValue; diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitConfig.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitConfig.java index 98052d34751..5d19d5b40d0 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitConfig.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitConfig.java @@ -15,6 +15,7 @@ public class RateLimitConfig { private List rateLimitRules = new ArrayList<>(); + private boolean rateLoggingEnabled = false; public RateLimitConfig() { } @@ -24,6 +25,16 @@ public RateLimitConfig(@JsonProperty("rateLimitRules") List rateL setRateLimitRules(rateLimitRules); } + @JsonProperty("rateLoggingEnabled") + public boolean isRateLoggingEnabled() { + return rateLoggingEnabled; + } + + @JsonProperty("rateLoggingEnabled") + public void setRateLoggingEnabled(boolean rateLoggingEnabled) { + this.rateLoggingEnabled = rateLoggingEnabled; + } + @JsonProperty("rateLimitRules") public List getRateLimitRules() { return rateLimitRules == null ? Collections.emptyList() : Collections.unmodifiableList(rateLimitRules); @@ -51,6 +62,7 @@ public int hashCode() { public String toString() { return "RateLimitConfig{" + "rateLimitRules=" + getRateLimitRules() + + "rateLoggingEnabled=" + rateLoggingEnabled + '}'; } } From 889378fa23de63ff481ac6884e498b42fd387a30 Mon Sep 17 00:00:00 2001 From: yuriyz Date: Tue, 6 Jan 2026 13:06:57 +0200 Subject: [PATCH 6/8] added rate limiting context Signed-off-by: YuriyZ Signed-off-by: yuriyz --- .../jans/as/server/rate/RateLimitContext.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitContext.java diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitContext.java b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitContext.java new file mode 100644 index 00000000000..924b78da2e7 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/rate/RateLimitContext.java @@ -0,0 +1,40 @@ +package io.jans.as.server.rate; + +import jakarta.servlet.http.HttpServletRequest; + +import java.io.IOException; + +public class RateLimitContext { + + private final HttpServletRequest request; + private final boolean rateLoggingEnabled; + private CachedBodyHttpServletRequest cachedRequest; + + public RateLimitContext(HttpServletRequest request, boolean rateLoggingEnabled) { + this.request = request; + this.rateLoggingEnabled = rateLoggingEnabled; + } + + public HttpServletRequest getRequest() { + return request; + } + + public boolean isRateLoggingEnabled() { + return rateLoggingEnabled; + } + + public boolean isCachedRequestAvailable() { + return cachedRequest != null; + } + + public CachedBodyHttpServletRequest getCachedRequest() throws IOException { + if (cachedRequest == null) { + cachedRequest = new CachedBodyHttpServletRequest(request); + } + return cachedRequest; + } + + public void setCachedRequest(CachedBodyHttpServletRequest cachedRequest) { + this.cachedRequest = cachedRequest; + } +} From 99b2b9b22a5bd5642870342ff1bde27231f0ae1d Mon Sep 17 00:00:00 2001 From: yuriyz Date: Wed, 7 Jan 2026 13:01:58 +0200 Subject: [PATCH 7/8] Added test servlet input stream for comprehensive rate limiting testing Signed-off-by: YuriyZ Signed-off-by: yuriyz --- .../server/rate/TestServletInputStream.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 jans-auth-server/server/src/test/java/io/jans/as/server/rate/TestServletInputStream.java diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/rate/TestServletInputStream.java b/jans-auth-server/server/src/test/java/io/jans/as/server/rate/TestServletInputStream.java new file mode 100644 index 00000000000..1846f531f86 --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/rate/TestServletInputStream.java @@ -0,0 +1,40 @@ +package io.jans.as.server.rate; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +public class TestServletInputStream extends ServletInputStream { + + private final ByteArrayInputStream bodyInputStream; + + public TestServletInputStream(String body) { + this(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + } + + public TestServletInputStream(ByteArrayInputStream bodyInputStream) { + this.bodyInputStream = bodyInputStream; + } + + @Override + public int read() { + return bodyInputStream.read(); + } + + @Override + public boolean isFinished() { + return bodyInputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + +} From 3b1d75983b37339aaba6140011fd4e0c7296c304 Mon Sep 17 00:00:00 2001 From: yuriyz Date: Fri, 9 Jan 2026 15:27:11 +0200 Subject: [PATCH 8/8] Renamings to avoid confusion for rate liming rules Signed-off-by: YuriyZ Signed-off-by: yuriyz --- .../configuration/rate/RateLimitRule.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitRule.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitRule.java index fb140c15396..7f0dc65ea51 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitRule.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/rate/RateLimitRule.java @@ -14,7 +14,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class RateLimitRule { - private String endpoint; + private String path; private List methods = new ArrayList<>(); private Integer requestCount; private Integer periodInSeconds; @@ -25,27 +25,27 @@ public RateLimitRule() { @JsonCreator public RateLimitRule( - @JsonProperty("endpoint") String endpoint, + @JsonProperty("path") String path, @JsonProperty("methods") List methods, @JsonProperty("requestCount") Integer requestCount, @JsonProperty("periodInSeconds") Integer periodInSeconds, @JsonProperty("keyExtractors") List keyExtractors ) { - setEndpoint(endpoint); + setPath(path); setMethods(methods); setRequestCount(requestCount); setPeriodInSeconds(periodInSeconds); setKeyExtractors(keyExtractors); } - @JsonProperty("endpoint") - public String getEndpoint() { - return endpoint; + @JsonProperty("path") + public String getPath() { + return path; } - @JsonProperty("endpoint") - public void setEndpoint(String endpoint) { - this.endpoint = (endpoint == null || endpoint.trim().isEmpty()) ? null : endpoint.trim(); + @JsonProperty("path") + public void setPath(String path) { + this.path = (path == null || path.trim().isEmpty()) ? null : path.trim(); } @JsonProperty("methods") @@ -107,7 +107,7 @@ public void setKeyExtractors(List keyExtractors) { * Non-throwing helper for validation-style checks in business logic. */ public boolean isWellFormed() { - return endpoint != null + return path != null && !getMethods().isEmpty() && requestCount != null && periodInSeconds != null @@ -119,7 +119,7 @@ public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof RateLimitRule)) return false; RateLimitRule that = (RateLimitRule) o; - return Objects.equals(endpoint, that.endpoint) + return Objects.equals(path, that.path) && Objects.equals(getMethods(), that.getMethods()) && Objects.equals(requestCount, that.requestCount) && Objects.equals(periodInSeconds, that.periodInSeconds) @@ -128,13 +128,13 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(endpoint, getMethods(), requestCount, periodInSeconds, getKeyExtractors()); + return Objects.hash(path, getMethods(), requestCount, periodInSeconds, getKeyExtractors()); } @Override public String toString() { return "RateLimitRule{" + - "endpoint='" + endpoint + '\'' + + "path='" + path + '\'' + ", methods=" + getMethods() + ", requestCount=" + requestCount + ", periodInSeconds=" + periodInSeconds +