standard_id 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/standard_id/rate_limit_handling.rb +72 -2
- data/app/controllers/standard_id/api/passwordless_controller.rb +9 -2
- data/app/controllers/standard_id/web/login_controller.rb +17 -5
- data/app/controllers/standard_id/web/reset_password/start_controller.rb +9 -2
- data/app/controllers/standard_id/web/verify_email/start_controller.rb +9 -2
- data/app/controllers/standard_id/web/verify_phone/start_controller.rb +9 -2
- data/app/helpers/standard_id/application_helper.rb +8 -0
- data/app/views/standard_id/web/login/show.html.erb +25 -2
- data/app/views/standard_id/web/login_verify/show.html.erb +48 -0
- data/lib/generators/standard_id/install/templates/standard_id.rb +8 -2
- data/lib/standard_id/config/schema.rb +26 -4
- data/lib/standard_id/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e92eb10b6ca75511deb6b2b825b95fc79b25db73135d1cc6c637095e7466f8e8
|
|
4
|
+
data.tar.gz: 4968ead0789aaeddb2ab68de34c224d951d988a18932e49e29125489cfa6eacb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c14b2551f993b7b7f9a20694ef337af4a9ebb05270c9a1cc343f43d3f3483d6196d1360786410b4140cc6c0214e82feadf5688019c347f66e0476958fb3c7c3
|
|
7
|
+
data.tar.gz: b71083067ee12c30bfedc1a1ee5ac8b9181c47fcaf790e8ee60e00eee4d73ac5a1eadcbd496e51f7244fd08bd83ec0a5a412e2cc225901c259809645b482af9a
|
|
@@ -4,20 +4,89 @@ module StandardId
|
|
|
4
4
|
|
|
5
5
|
RATE_LIMIT_STORE = StandardId::RateLimitStore.new
|
|
6
6
|
|
|
7
|
+
# Fallback window for the Retry-After header when the tripped limit's window
|
|
8
|
+
# was not captured on the instance — e.g. a hand-rolled `raise
|
|
9
|
+
# TooManyRequests` that bypasses the rate_limit macro (see
|
|
10
|
+
# Api::Oauth::TokensController's per-audience limit, which uses a 15-minute
|
|
11
|
+
# window). Matches that hand-rolled limit's window, so the fallback stays
|
|
12
|
+
# accurate for it.
|
|
13
|
+
DEFAULT_RETRY_AFTER = 15.minutes
|
|
14
|
+
|
|
7
15
|
included do
|
|
8
16
|
rescue_from ActionController::TooManyRequests, with: :handle_rate_limited
|
|
9
17
|
end
|
|
10
18
|
|
|
19
|
+
class_methods do
|
|
20
|
+
# Wrap Rails' `rate_limit` so the tripped limit's window (`within:`) is
|
|
21
|
+
# remembered on the controller instance, letting the shared
|
|
22
|
+
# `handle_rate_limited` rescue emit a Retry-After that reflects the ACTUAL
|
|
23
|
+
# window instead of a hardcoded 15 minutes (which was 4x wrong for every
|
|
24
|
+
# 1-hour limit — verification/password-reset/signup/api-passwordless start,
|
|
25
|
+
# dynamic registration). This transparently upgrades every existing
|
|
26
|
+
# `rate_limit ... within: X` call site with no change to the call itself.
|
|
27
|
+
#
|
|
28
|
+
# ActionController::TooManyRequests carries no window, and a controller may
|
|
29
|
+
# declare several limits with different windows, so a single class-level
|
|
30
|
+
# value can't identify which limit fired. Capturing it in the per-limit
|
|
31
|
+
# `with:` closure (evaluated in controller context the instant that
|
|
32
|
+
# specific limit trips) is the least-invasive way to thread the correct
|
|
33
|
+
# window through. A host that passes its own `with:` opts out and keeps the
|
|
34
|
+
# default fallback.
|
|
35
|
+
def rate_limit(within:, with: nil, **options)
|
|
36
|
+
with ||= -> {
|
|
37
|
+
@standard_id_rate_limit_within = within
|
|
38
|
+
raise ActionController::TooManyRequests
|
|
39
|
+
}
|
|
40
|
+
super(within: within, with: with, **options)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Resolve the effective per-IP login rate limit, preferring the
|
|
45
|
+
# mechanism-agnostic `login_per_ip` alias and falling back to the deprecated
|
|
46
|
+
# `password_login_per_ip` when the host left the alias at its default (i.e.
|
|
47
|
+
# only configured the old name). The new alias wins whenever explicitly set.
|
|
48
|
+
# Mirrors the max_attempts -> max_attempts_per_challenge deprecation-alias
|
|
49
|
+
# precedent (prefer-new, fall-back-to-old).
|
|
50
|
+
def self.login_per_ip
|
|
51
|
+
resolve_login_alias(:login_per_ip, :password_login_per_ip)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Effective per-email login rate limit; see .login_per_ip.
|
|
55
|
+
def self.login_per_email
|
|
56
|
+
resolve_login_alias(:login_per_email, :password_login_per_email)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Return the alias value unless it still equals its schema default, in which
|
|
60
|
+
# case fall back to the deprecated field. Both fields share the same default,
|
|
61
|
+
# so the effective default is unchanged when neither is set.
|
|
62
|
+
def self.resolve_login_alias(new_field, old_field)
|
|
63
|
+
rate_limits = StandardId.config.rate_limits
|
|
64
|
+
default = StandardId.config.__schema__.field_for(:rate_limits, new_field).default_value
|
|
65
|
+
new_value = rate_limits[new_field]
|
|
66
|
+
new_value == default ? rate_limits[old_field] : new_value
|
|
67
|
+
end
|
|
68
|
+
|
|
11
69
|
private
|
|
12
70
|
|
|
13
71
|
def handle_rate_limited(_exception)
|
|
14
|
-
|
|
72
|
+
retry_after = (@standard_id_rate_limit_within || DEFAULT_RETRY_AFTER).to_i
|
|
73
|
+
response.set_header("Retry-After", retry_after.to_s)
|
|
15
74
|
|
|
16
75
|
if self.class.ancestors.include?(ActionController::API)
|
|
17
76
|
render json: {
|
|
18
77
|
error: "rate_limit_exceeded",
|
|
19
78
|
error_description: "Too many requests. Please try again later."
|
|
20
79
|
}, status: :too_many_requests
|
|
80
|
+
elsif request.get? || request.head?
|
|
81
|
+
# A rate-limited GET/HEAD has no sibling form to bounce to. Redirecting
|
|
82
|
+
# to `request.path` (the non-GET branch below) would target the SAME
|
|
83
|
+
# throttled action, so the browser follows the redirect, re-increments
|
|
84
|
+
# the counter, and gets redirected again — an unbounded loop that also
|
|
85
|
+
# keeps resetting the window. v0.28.0 shipped the first rate-limited GETs
|
|
86
|
+
# (email/phone confirm #show), which exposed this. Render a terminal 429
|
|
87
|
+
# instead: the response is the end of the exchange, so it cannot loop.
|
|
88
|
+
render plain: "Too many requests. Please try again later.",
|
|
89
|
+
status: :too_many_requests
|
|
21
90
|
else
|
|
22
91
|
flash[:alert] = "Too many requests. Please try again later."
|
|
23
92
|
# Bounce back to the rate-limited form's own GET action. Previously this
|
|
@@ -26,7 +95,8 @@ module StandardId
|
|
|
26
95
|
# API/control-plane that only mounts the engine — `main_app.root_path`
|
|
27
96
|
# then doesn't exist), and a cross-origin `Referer` (Rails refuses the
|
|
28
97
|
# redirect as unsafe). `request.path` is always a valid, same-origin GET
|
|
29
|
-
# for every rate-limited action here, so it degrades
|
|
98
|
+
# for every rate-limited *non-GET* action here, so it degrades
|
|
99
|
+
# gracefully. (GET/HEAD are handled above to avoid a redirect loop.)
|
|
30
100
|
redirect_to request.path, status: :see_other
|
|
31
101
|
end
|
|
32
102
|
end
|
|
@@ -12,10 +12,17 @@ module StandardId
|
|
|
12
12
|
only: :start,
|
|
13
13
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
14
14
|
|
|
15
|
-
# RAR-60: Rate limit OTP initiation by target (5 per 15 minutes)
|
|
15
|
+
# RAR-60: Rate limit OTP initiation by target (5 per 15 minutes). A blank
|
|
16
|
+
# target would collapse into one shared "api-passwordless:" bucket
|
|
17
|
+
# (`.compact` does not drop a non-nil empty string), throttling every
|
|
18
|
+
# caller; fall the key back to the remote IP when the target is blank so
|
|
19
|
+
# blank spam stays bounded per-IP without poisoning real targets.
|
|
16
20
|
rate_limit to: StandardId.config.rate_limits.api_passwordless_start_per_target,
|
|
17
21
|
within: 15.minutes,
|
|
18
|
-
by: -> {
|
|
22
|
+
by: -> {
|
|
23
|
+
target = (params[:username] || params[:email] || params[:phone_number]).to_s.strip.downcase
|
|
24
|
+
"api-passwordless:#{target.presence || request.remote_ip}"
|
|
25
|
+
},
|
|
19
26
|
name: "passwordless-target",
|
|
20
27
|
only: :start,
|
|
21
28
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
@@ -10,17 +10,29 @@ module StandardId
|
|
|
10
10
|
|
|
11
11
|
layout "public"
|
|
12
12
|
|
|
13
|
-
# RAR-51: Rate limit login attempts by IP (20 per 15 minutes)
|
|
14
|
-
|
|
13
|
+
# RAR-51: Rate limit login attempts by IP (20 per 15 minutes). Reads the
|
|
14
|
+
# mechanism-agnostic `login_per_ip` alias, falling back to the deprecated
|
|
15
|
+
# `password_login_per_ip` — this action branches password OR passwordless,
|
|
16
|
+
# so on a passwordless app this governs the OTP-send limit, not a password
|
|
17
|
+
# login. See StandardId::RateLimitHandling.login_per_ip.
|
|
18
|
+
rate_limit to: StandardId::RateLimitHandling.login_per_ip,
|
|
15
19
|
within: 15.minutes,
|
|
16
20
|
name: "login-ip",
|
|
17
21
|
only: :create,
|
|
18
22
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
19
23
|
|
|
20
|
-
# RAR-51: Rate limit login attempts by email target (5 per 15 minutes)
|
|
21
|
-
|
|
24
|
+
# RAR-51: Rate limit login attempts by email target (5 per 15 minutes).
|
|
25
|
+
# A blank email would otherwise collapse every blank-target request into
|
|
26
|
+
# one shared "login-email:" bucket (`.compact` does not drop a non-nil
|
|
27
|
+
# empty string), so blank spam throttles all users. Fall the key back to
|
|
28
|
+
# the remote IP when the target is blank, keeping it bounded per-IP while
|
|
29
|
+
# a well-formed email keeps its own per-target bucket.
|
|
30
|
+
rate_limit to: StandardId::RateLimitHandling.login_per_email,
|
|
22
31
|
within: 15.minutes,
|
|
23
|
-
by: -> {
|
|
32
|
+
by: -> {
|
|
33
|
+
email = params.dig(:login, :email).to_s.strip.downcase
|
|
34
|
+
"login-email:#{email.presence || request.remote_ip}"
|
|
35
|
+
},
|
|
24
36
|
name: "login-email",
|
|
25
37
|
only: :create,
|
|
26
38
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
@@ -16,10 +16,17 @@ module StandardId
|
|
|
16
16
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
17
17
|
|
|
18
18
|
# Rate limit reset requests by email target (3 per 15 minutes) to blunt
|
|
19
|
-
# account-enumeration probing of individual addresses.
|
|
19
|
+
# account-enumeration probing of individual addresses. A blank email
|
|
20
|
+
# would collapse into one shared "reset-password:" bucket (`.compact`
|
|
21
|
+
# does not drop a non-nil empty string), throttling everyone; fall the
|
|
22
|
+
# key back to the remote IP when blank so it stays bounded per-IP without
|
|
23
|
+
# poisoning real targets.
|
|
20
24
|
rate_limit to: StandardId.config.rate_limits.password_reset_start_per_target,
|
|
21
25
|
within: 15.minutes,
|
|
22
|
-
by: -> {
|
|
26
|
+
by: -> {
|
|
27
|
+
email = params[:email].to_s.strip.downcase
|
|
28
|
+
"reset-password:#{email.presence || request.remote_ip}"
|
|
29
|
+
},
|
|
23
30
|
name: "reset-password-target",
|
|
24
31
|
only: :create,
|
|
25
32
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
@@ -9,10 +9,17 @@ module StandardId
|
|
|
9
9
|
only: :create,
|
|
10
10
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
11
11
|
|
|
12
|
-
# RAR-56: Rate limit verification code generation by email target (3 per
|
|
12
|
+
# RAR-56: Rate limit verification code generation by email target (3 per
|
|
13
|
+
# 15 minutes). A blank email would collapse into one shared
|
|
14
|
+
# "verify-email:" bucket (`.compact` does not drop a non-nil empty
|
|
15
|
+
# string), throttling everyone; fall the key back to the remote IP when
|
|
16
|
+
# blank so it stays bounded per-IP without poisoning real targets.
|
|
13
17
|
rate_limit to: StandardId.config.rate_limits.verification_start_per_target,
|
|
14
18
|
within: 15.minutes,
|
|
15
|
-
by: -> {
|
|
19
|
+
by: -> {
|
|
20
|
+
email = params[:email].to_s.strip.downcase
|
|
21
|
+
"verify-email:#{email.presence || request.remote_ip}"
|
|
22
|
+
},
|
|
16
23
|
name: "verify-email-target",
|
|
17
24
|
only: :create,
|
|
18
25
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
@@ -9,10 +9,17 @@ module StandardId
|
|
|
9
9
|
only: :create,
|
|
10
10
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
11
11
|
|
|
12
|
-
# RAR-56: Rate limit verification code generation by phone target (3 per
|
|
12
|
+
# RAR-56: Rate limit verification code generation by phone target (3 per
|
|
13
|
+
# 15 minutes). A blank phone would collapse into one shared
|
|
14
|
+
# "verify-phone:" bucket (`.compact` does not drop a non-nil empty
|
|
15
|
+
# string), throttling everyone; fall the key back to the remote IP when
|
|
16
|
+
# blank so it stays bounded per-IP without poisoning real targets.
|
|
13
17
|
rate_limit to: StandardId.config.rate_limits.verification_start_per_target,
|
|
14
18
|
within: 15.minutes,
|
|
15
|
-
by: -> {
|
|
19
|
+
by: -> {
|
|
20
|
+
phone = params[:phone_number].to_s.strip
|
|
21
|
+
"verify-phone:#{phone.presence || request.remote_ip}"
|
|
22
|
+
},
|
|
16
23
|
name: "verify-phone-target",
|
|
17
24
|
only: :create,
|
|
18
25
|
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
@@ -6,5 +6,13 @@ module StandardId
|
|
|
6
6
|
def otp_code_length
|
|
7
7
|
StandardId::Passwordless.otp_code_length
|
|
8
8
|
end
|
|
9
|
+
|
|
10
|
+
# The configured OTP resend cooldown, in seconds. The native login_verify
|
|
11
|
+
# view uses this to drive a client-side "resend in Ns" countdown that mirrors
|
|
12
|
+
# the server-side cooldown (passwordless.retry_delay, enforced in
|
|
13
|
+
# BaseStrategy#enforce_retry_delay!). 0 or negative means no cooldown.
|
|
14
|
+
def otp_retry_delay
|
|
15
|
+
StandardId::Passwordless.retry_delay
|
|
16
|
+
end
|
|
9
17
|
end
|
|
10
18
|
end
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<p role="status"><%= flash[:notice] %></p>
|
|
18
18
|
<% end %>
|
|
19
19
|
|
|
20
|
-
<%= form_with url: login_path, method: :post, local: true do |form| %>
|
|
20
|
+
<%= form_with url: login_path, method: :post, local: true, html: { id: "passwordless-login-form" } do |form| %>
|
|
21
21
|
<%= form.hidden_field :redirect_uri, value: @redirect_uri %>
|
|
22
22
|
|
|
23
23
|
<p>
|
|
@@ -26,10 +26,33 @@
|
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
28
|
<p>
|
|
29
|
-
<%= form.submit "Continue" %>
|
|
29
|
+
<%= form.submit "Continue", data: { sending_label: "Sending…" } %>
|
|
30
30
|
</p>
|
|
31
31
|
<% end %>
|
|
32
32
|
|
|
33
|
+
<%#
|
|
34
|
+
Progressive-enhancement double-submit guard. With JS off the form submits
|
|
35
|
+
normally; with JS on we disable the submit button on the first submit so an
|
|
36
|
+
impatient double-click can't fire two OTP requests. Disabling happens IN the
|
|
37
|
+
submit handler — the browser has already serialized the form for the native
|
|
38
|
+
(non-AJAX) submit by then, so the request still goes through. Vanilla JS
|
|
39
|
+
only; the gem ships no JS framework.
|
|
40
|
+
%>
|
|
41
|
+
<script>
|
|
42
|
+
(function () {
|
|
43
|
+
var form = document.getElementById("passwordless-login-form");
|
|
44
|
+
if (!form) return;
|
|
45
|
+
form.addEventListener("submit", function () {
|
|
46
|
+
var btn = form.querySelector('input[type="submit"], button[type="submit"]');
|
|
47
|
+
if (!btn || btn.dataset.stdidSubmitting === "true") return;
|
|
48
|
+
btn.dataset.stdidSubmitting = "true";
|
|
49
|
+
btn.disabled = true;
|
|
50
|
+
var label = btn.getAttribute("data-sending-label") || "Sending…";
|
|
51
|
+
if (btn.tagName === "INPUT") { btn.value = label; } else { btn.textContent = label; }
|
|
52
|
+
});
|
|
53
|
+
})();
|
|
54
|
+
</script>
|
|
55
|
+
|
|
33
56
|
<% if StandardId.config.google_client_id.present? || StandardId.config.apple_client_id.present? %>
|
|
34
57
|
<%= render "standard_id/web/login/social_buttons" %>
|
|
35
58
|
<% end %>
|
|
@@ -30,6 +30,54 @@
|
|
|
30
30
|
<%= form.submit "Verify", class: "flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500" %>
|
|
31
31
|
</div>
|
|
32
32
|
<% end %>
|
|
33
|
+
|
|
34
|
+
<%#
|
|
35
|
+
Resend code. Re-runs the passwordless start path (POST /login with the
|
|
36
|
+
same email), which re-issues an OTP and redirects back here. The server
|
|
37
|
+
enforces a resend cooldown (passwordless.retry_delay) and rejects an early
|
|
38
|
+
resend with a flash alert, so this is safe with JS off. With JS on we mirror
|
|
39
|
+
that cooldown as a visible "Resend in Ns" countdown and keep the button
|
|
40
|
+
disabled until it hits 0. @otp_data comes from require_otp_payload!.
|
|
41
|
+
%>
|
|
42
|
+
<div class="mt-6 text-center text-sm/6 text-gray-500 dark:text-gray-400">
|
|
43
|
+
<%= form_with url: login_path, method: :post, local: true, html: { id: "resend-otp-form", class: "inline" } do |resend_form| %>
|
|
44
|
+
<%= resend_form.hidden_field "login[email]", value: @otp_data[:username] %>
|
|
45
|
+
Didn't get a code?
|
|
46
|
+
<button type="submit" id="resend-otp-button" data-retry-delay="<%= otp_retry_delay %>" class="font-semibold text-indigo-600 hover:text-indigo-500 disabled:cursor-not-allowed disabled:text-gray-400 dark:text-indigo-400 dark:hover:text-indigo-300 dark:disabled:text-gray-500">
|
|
47
|
+
Resend code
|
|
48
|
+
</button>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
33
51
|
</div>
|
|
34
52
|
</div>
|
|
35
53
|
</div>
|
|
54
|
+
|
|
55
|
+
<%#
|
|
56
|
+
Client-side resend cooldown. Progressive enhancement: with JS off the button is
|
|
57
|
+
always active and the server's retry_delay guard is the backstop. Vanilla JS only.
|
|
58
|
+
%>
|
|
59
|
+
<script>
|
|
60
|
+
(function () {
|
|
61
|
+
var btn = document.getElementById("resend-otp-button");
|
|
62
|
+
if (!btn) return;
|
|
63
|
+
var delay = parseInt(btn.getAttribute("data-retry-delay"), 10);
|
|
64
|
+
if (!delay || delay <= 0) return;
|
|
65
|
+
|
|
66
|
+
var label = "Resend code";
|
|
67
|
+
var remaining = delay;
|
|
68
|
+
|
|
69
|
+
function tick() {
|
|
70
|
+
if (remaining <= 0) {
|
|
71
|
+
btn.disabled = false;
|
|
72
|
+
btn.textContent = label;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
btn.disabled = true;
|
|
76
|
+
btn.textContent = "Resend in " + remaining + "s";
|
|
77
|
+
remaining -= 1;
|
|
78
|
+
window.setTimeout(tick, 1000);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
tick();
|
|
82
|
+
})();
|
|
83
|
+
</script>
|
|
@@ -362,8 +362,14 @@ StandardId.configure do |c|
|
|
|
362
362
|
# ---------------------------------------------------------------------------
|
|
363
363
|
# Defaults are conservative; tune per environment.
|
|
364
364
|
|
|
365
|
-
#
|
|
366
|
-
#
|
|
365
|
+
# Login limits. The login action branches password OR passwordless, so on a
|
|
366
|
+
# passwordless app these govern the OTP-SEND limit. Prefer the
|
|
367
|
+
# mechanism-agnostic names; the deprecated password_login_* names still work
|
|
368
|
+
# (the new name wins when both are set).
|
|
369
|
+
# c.rate_limits.login_per_ip = 20 # per 15 minutes
|
|
370
|
+
# c.rate_limits.login_per_email = 5 # per 15 minutes
|
|
371
|
+
# c.rate_limits.password_login_per_ip = 20 # deprecated alias of login_per_ip
|
|
372
|
+
# c.rate_limits.password_login_per_email = 5 # deprecated alias of login_per_email
|
|
367
373
|
# c.rate_limits.otp_verify_per_ip = 20 # per 15 minutes
|
|
368
374
|
# c.rate_limits.verification_start_per_target = 3 # per 15 minutes
|
|
369
375
|
# c.rate_limits.verification_start_per_ip = 10 # per hour
|
|
@@ -341,9 +341,24 @@ StandardId::ConfigSchema.define do
|
|
|
341
341
|
|
|
342
342
|
# Rate limiting defaults (used by Rails 8 built-in rate_limit DSL)
|
|
343
343
|
scope :rate_limits do
|
|
344
|
-
# RAR-51:
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
# RAR-51: Login rate limits.
|
|
345
|
+
#
|
|
346
|
+
# Mechanism-agnostic aliases. The `rate_limit` macro that consumes these
|
|
347
|
+
# sits on Web::LoginController#create, which branches password OR
|
|
348
|
+
# passwordless — so on a passwordless app these govern the OTP-SEND limit,
|
|
349
|
+
# not a password login. Prefer these names for new config.
|
|
350
|
+
field :login_per_ip, type: :integer, default: 20 # per 15 minutes
|
|
351
|
+
field :login_per_email, type: :integer, default: 5 # per 15 minutes
|
|
352
|
+
|
|
353
|
+
# Deprecated mechanism-specific names, retained for backwards compatibility.
|
|
354
|
+
# The schema raises ConfigurationError on unknown fields at boot, so these
|
|
355
|
+
# must NOT be removed while hosts still set them. When a host leaves the
|
|
356
|
+
# `login_per_*` alias at its default, the login controller falls back to
|
|
357
|
+
# these values (see StandardId::RateLimitHandling.login_per_ip). New name
|
|
358
|
+
# wins when explicitly set. Mirrors the max_attempts ->
|
|
359
|
+
# max_attempts_per_challenge deprecation-alias precedent.
|
|
360
|
+
field :password_login_per_ip, type: :integer, default: 20 # per 15 minutes; deprecated alias of login_per_ip
|
|
361
|
+
field :password_login_per_email, type: :integer, default: 5 # per 15 minutes; deprecated alias of login_per_email
|
|
347
362
|
|
|
348
363
|
# RAR-60: OTP verification
|
|
349
364
|
field :otp_verify_per_ip, type: :integer, default: 20 # per 15 minutes
|
|
@@ -361,9 +376,16 @@ StandardId::ConfigSchema.define do
|
|
|
361
376
|
# Password signup — throttle account-creation spam by IP.
|
|
362
377
|
field :signup_per_ip, type: :integer, default: 10 # per hour
|
|
363
378
|
|
|
364
|
-
# API
|
|
379
|
+
# API OTP-initiation limits — the API counterpart of the web *login* limits
|
|
380
|
+
# (login_per_* / the deprecated password_login_*): the API passwordless
|
|
381
|
+
# `start` action and the web login action both drive the passwordless
|
|
382
|
+
# `start!` strategy. NOT the counterpart of verification_start_*
|
|
383
|
+
# (email/phone verification), which bypasses the strategy via a direct
|
|
384
|
+
# CodeChallenge.create!.
|
|
365
385
|
field :api_passwordless_start_per_ip, type: :integer, default: 10 # per hour
|
|
366
386
|
field :api_passwordless_start_per_target, type: :integer, default: 5 # per 15 minutes
|
|
387
|
+
|
|
388
|
+
# OAuth token endpoint — requests per IP.
|
|
367
389
|
field :api_token_per_ip, type: :integer, default: 30 # per 15 minutes
|
|
368
390
|
|
|
369
391
|
# Optional per-audience tightening on top of the api_token_per_ip
|
data/lib/standard_id/version.rb
CHANGED