standard_id 0.27.0 → 0.28.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d2917c7c97e54e0fd55a78e52c0f8eb202e3e1221267d5fe25af54da788c0e85
4
- data.tar.gz: 7d2dad4ae7677431a6df91efef81ef0cf08794d7adc26be6b26fede63247f358
3
+ metadata.gz: c3aa81db933d1f506926c8c400e2bc57f60a7b29529f7bfa514e71b68fe028c1
4
+ data.tar.gz: 47a3be048ef63b02ee3819354dae0e4af75cb21a88d740e5b72e754c55471a2a
5
5
  SHA512:
6
- metadata.gz: 5bc7d5bc447389b6cb44ae9962820fcb6a4bfb2109dc1e82fb334cfdba08d8dab1c1f8d7c41e63ee47ec8962b3a2ee38bd59d02ada3177b6ede3adce246398a2
7
- data.tar.gz: 7d27375bfce3b17ff6ede7690f6e114f0e84d374c792e02d6c847f90a8a5a6d4c5492a57df596a94d8dbae073aa39c67627498c23c05ddb3639517d580ad4437
6
+ metadata.gz: e4b6618506a9428d7a426ad4d095bd95fd2aa9c3f7b58e8727f20e8300d8dc3c6f622da1e07829961474c392d35a292775f4596b36e39c0961f66a28cca2db7a
7
+ data.tar.gz: 585a4b2d8c65e4d9de10eb8466d99c9323ea6279459edf21fe72705f829688cded440133faaa50317a01bcf4d2544ecf48b01749527287be73cc042eb833fe6a
@@ -7,6 +7,23 @@ module StandardId
7
7
 
8
8
  layout "public"
9
9
 
10
+ # Rate limit reset-request generation by IP (10 per hour). The endpoint
11
+ # emails a reset token, so without a limit it's an email-flooding vector.
12
+ rate_limit to: StandardId.config.rate_limits.password_reset_start_per_ip,
13
+ within: 1.hour,
14
+ name: "reset-password-ip",
15
+ only: :create,
16
+ store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
17
+
18
+ # Rate limit reset requests by email target (3 per 15 minutes) to blunt
19
+ # account-enumeration probing of individual addresses.
20
+ rate_limit to: StandardId.config.rate_limits.password_reset_start_per_target,
21
+ within: 15.minutes,
22
+ by: -> { "reset-password:#{params[:email].to_s.strip.downcase}" },
23
+ name: "reset-password-target",
24
+ only: :create,
25
+ store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
26
+
10
27
  skip_before_action :require_browser_session!, only: [:show, :create]
11
28
 
12
29
  def show
@@ -9,6 +9,13 @@ module StandardId
9
9
 
10
10
  layout "public"
11
11
 
12
+ # Throttle account-creation spam by IP (10 per hour).
13
+ rate_limit to: StandardId.config.rate_limits.signup_per_ip,
14
+ within: 1.hour,
15
+ name: "signup-ip",
16
+ only: :create,
17
+ store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
18
+
12
19
  skip_before_action :require_browser_session!, only: [:show, :create]
13
20
 
14
21
  before_action :redirect_if_authenticated, only: [:show]
@@ -2,6 +2,15 @@ module StandardId
2
2
  module Web
3
3
  module VerifyEmail
4
4
  class ConfirmController < BaseController
5
+ # Per-IP limit on code confirmation (20 per 15 minutes, shared with OTP
6
+ # verify). The per-challenge attempt cap alone doesn't stop distributed
7
+ # guessing across many challenges; both show and update probe a code.
8
+ rate_limit to: StandardId.config.rate_limits.otp_verify_per_ip,
9
+ within: 15.minutes,
10
+ name: "verify-email-confirm-ip",
11
+ only: [:show, :update],
12
+ store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
13
+
5
14
  before_action :prepare_code_challenge
6
15
 
7
16
  def show
@@ -2,6 +2,15 @@ module StandardId
2
2
  module Web
3
3
  module VerifyPhone
4
4
  class ConfirmController < BaseController
5
+ # Per-IP limit on code confirmation (20 per 15 minutes, shared with OTP
6
+ # verify). The per-challenge attempt cap alone doesn't stop distributed
7
+ # guessing across many challenges; both show and update probe a code.
8
+ rate_limit to: StandardId.config.rate_limits.otp_verify_per_ip,
9
+ within: 15.minutes,
10
+ name: "verify-phone-confirm-ip",
11
+ only: [:show, :update],
12
+ store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
13
+
5
14
  before_action :prepare_code_challenge
6
15
 
7
16
  def show
@@ -153,7 +153,10 @@ StandardId.configure do |c|
153
153
  # for backwards compatibility; new installs should set the newer key.
154
154
  # c.passwordless.max_attempts = 3
155
155
 
156
- # Default: 30 — seconds before a new challenge can be requested.
156
+ # Default: 30 — OTP-resend cooldown: minimum seconds before the same target
157
+ # can request another code. Enforced in the passwordless start path; a resend
158
+ # inside the window is rejected (the already-sent code stays valid). Set to 0
159
+ # to disable the cooldown.
157
160
  # c.passwordless.retry_delay = 30
158
161
 
159
162
  # Custom username validator; return nil/false to accept, or an error message
@@ -352,6 +352,15 @@ StandardId::ConfigSchema.define do
352
352
  field :verification_start_per_target, type: :integer, default: 3 # per 15 minutes
353
353
  field :verification_start_per_ip, type: :integer, default: 10 # per hour
354
354
 
355
+ # Password-reset request. The start endpoint emails a reset token, so an
356
+ # unthrottled endpoint is an email-flooding + account-enumeration vector.
357
+ # Mirrors the verification_start shape (per-IP hourly + per-target 15-min).
358
+ field :password_reset_start_per_ip, type: :integer, default: 10 # per hour
359
+ field :password_reset_start_per_target, type: :integer, default: 3 # per 15 minutes
360
+
361
+ # Password signup — throttle account-creation spam by IP.
362
+ field :signup_per_ip, type: :integer, default: 10 # per hour
363
+
355
364
  # API equivalents
356
365
  field :api_passwordless_start_per_ip, type: :integer, default: 10 # per hour
357
366
  field :api_passwordless_start_per_target, type: :integer, default: 5 # per 15 minutes
@@ -23,6 +23,7 @@ module StandardId
23
23
 
24
24
  validate_username!(username)
25
25
  run_username_validator!(username)
26
+ enforce_retry_delay!(username)
26
27
  emit_code_requested(username)
27
28
  challenge = create_challenge!(
28
29
  username,
@@ -43,6 +44,29 @@ module StandardId
43
44
 
44
45
  protected
45
46
 
47
+ # Reject a resend that arrives within passwordless.retry_delay seconds of
48
+ # the most recent still-active code for this target. A soft cooldown that
49
+ # complements the coarser per-target rate limits: it does not consume the
50
+ # active challenge, so the code already sent remains usable. Raises
51
+ # InvalidRequestError (rescued by the web login + API OAuth error handlers).
52
+ def enforce_retry_delay!(username)
53
+ delay = StandardId::Passwordless.retry_delay
54
+ return if delay <= 0
55
+
56
+ last = StandardId::CodeChallenge.active
57
+ .where(realm: @realm, channel: connection_type, target: username)
58
+ .order(created_at: :desc)
59
+ .first
60
+ return if last.nil?
61
+
62
+ elapsed = Time.current - last.created_at
63
+ return if elapsed >= delay
64
+
65
+ wait = (delay - elapsed).ceil
66
+ raise StandardId::InvalidRequestError,
67
+ "Please wait #{wait} #{'second'.pluralize(wait)} before requesting another code"
68
+ end
69
+
46
70
  def create_challenge!(username, code_length: nil, expires_in: nil, metadata: {})
47
71
  ActiveRecord::Base.transaction do
48
72
  invalidate_active_challenges!(username)
@@ -83,6 +83,14 @@ module StandardId
83
83
  legacy = StandardId.config.passwordless.max_attempts.to_i
84
84
  legacy.positive? ? legacy : 5
85
85
  end
86
+
87
+ # Minimum seconds that must elapse between successive code requests for
88
+ # the same target (the OTP-resend cooldown). Reads passwordless.retry_delay;
89
+ # 0 or negative disables the cooldown. Enforced in the passwordless strategy
90
+ # start path (see BaseStrategy#enforce_retry_delay!).
91
+ def retry_delay
92
+ StandardId.config.passwordless.retry_delay.to_i
93
+ end
86
94
  end
87
95
  end
88
96
  end
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.27.0"
2
+ VERSION = "0.28.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.0
4
+ version: 0.28.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim