standard_id 0.9.0 → 0.10.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/inertia_rendering.rb +2 -1
- data/app/controllers/concerns/standard_id/rate_limit_handling.rb +27 -0
- data/app/controllers/standard_id/api/base_controller.rb +2 -0
- data/app/controllers/standard_id/api/oauth/tokens_controller.rb +6 -0
- data/app/controllers/standard_id/api/passwordless_controller.rb +15 -0
- data/app/controllers/standard_id/web/base_controller.rb +1 -0
- data/app/controllers/standard_id/web/login_controller.rb +15 -0
- data/app/controllers/standard_id/web/login_verify_controller.rb +28 -4
- data/app/controllers/standard_id/web/verify_email/start_controller.rb +15 -0
- data/app/controllers/standard_id/web/verify_phone/start_controller.rb +15 -0
- data/app/mailers/standard_id/passwordless_mailer.rb +16 -0
- data/app/views/standard_id/passwordless_mailer/otp_email.html.erb +24 -0
- data/app/views/standard_id/passwordless_mailer/otp_email.text.erb +12 -0
- data/lib/generators/standard_id/install/templates/standard_id.rb +5 -0
- data/lib/standard_id/config/schema.rb +40 -0
- data/lib/standard_id/engine.rb +1 -0
- data/lib/standard_id/events/subscribers/passwordless_delivery_subscriber.rb +37 -0
- data/lib/standard_id/jwt_service.rb +4 -3
- data/lib/standard_id/oauth/token_grant_flow.rb +17 -1
- data/lib/standard_id/passwordless/base_strategy.rb +34 -2
- data/lib/standard_id/passwordless/email_strategy.rb +7 -0
- data/lib/standard_id/passwordless/sms_strategy.rb +5 -0
- data/lib/standard_id/passwordless/verification_service.rb +56 -16
- data/lib/standard_id/passwordless.rb +56 -0
- data/lib/standard_id/rate_limit_store.rb +42 -0
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id.rb +3 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ad192cf3b1bad92d1ec8322e203804401d8c10e54ebfcde4340f7840fa624bd
|
|
4
|
+
data.tar.gz: 0eb0cc7c613bd3fdd981818c6d4fd359f809003da34d6e15f5e2fea9fd0eab74
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 478082fd5a80a66ded10834c7bd1db32912a222086f5fc8db521a90acceb1459418492581d89ca90d5a7d8fb438b3c74845372fe551d52eb892c297136acc002
|
|
7
|
+
data.tar.gz: bca63014a4ca013f152bb5bd9112f3649d0e7848d22b7d1114e9bd2715e2847369610e735b83f20bda3aad3fca45936d0f7452438f2a8144d24ddba7b3cb2c07
|
|
@@ -57,7 +57,8 @@ module StandardId
|
|
|
57
57
|
password_reset: web.password_reset,
|
|
58
58
|
email_verification: web.email_verification,
|
|
59
59
|
phone_verification: web.phone_verification,
|
|
60
|
-
sessions_management: web.sessions_management
|
|
60
|
+
sessions_management: web.sessions_management,
|
|
61
|
+
passwordless_registration: web.passwordless_registration
|
|
61
62
|
}
|
|
62
63
|
end
|
|
63
64
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module RateLimitHandling
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
RATE_LIMIT_STORE = StandardId::RateLimitStore.new
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
rescue_from ActionController::TooManyRequests, with: :handle_rate_limited
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def handle_rate_limited(_exception)
|
|
14
|
+
response.set_header("Retry-After", 15.minutes.to_i.to_s)
|
|
15
|
+
|
|
16
|
+
if self.class.ancestors.include?(ActionController::API)
|
|
17
|
+
render json: {
|
|
18
|
+
error: "rate_limit_exceeded",
|
|
19
|
+
error_description: "Too many requests. Please try again later."
|
|
20
|
+
}, status: :too_many_requests
|
|
21
|
+
else
|
|
22
|
+
flash[:alert] = "Too many requests. Please try again later."
|
|
23
|
+
redirect_to request.referer || main_app.root_path, status: :see_other
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
module StandardId
|
|
2
2
|
module Api
|
|
3
3
|
class BaseController < ActionController::API
|
|
4
|
+
include ActionController::RateLimiting
|
|
4
5
|
include StandardId::ControllerPolicy
|
|
5
6
|
include StandardId::ApiAuthentication
|
|
6
7
|
include StandardId::SetCurrentRequestDetails
|
|
8
|
+
include StandardId::RateLimitHandling
|
|
7
9
|
|
|
8
10
|
before_action -> { Current.scope = :api if defined?(::Current) }
|
|
9
11
|
before_action :validate_content_type!
|
|
@@ -6,6 +6,12 @@ module StandardId
|
|
|
6
6
|
|
|
7
7
|
skip_before_action :validate_content_type!
|
|
8
8
|
|
|
9
|
+
# RAR-51/RAR-60: Rate limit token requests by IP (30 per 15 minutes)
|
|
10
|
+
rate_limit to: StandardId.config.rate_limits.api_token_per_ip,
|
|
11
|
+
within: 15.minutes,
|
|
12
|
+
only: :create,
|
|
13
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
14
|
+
|
|
9
15
|
FLOW_STRATEGIES = {
|
|
10
16
|
"client_credentials" => StandardId::Oauth::ClientCredentialsFlow,
|
|
11
17
|
"authorization_code" => StandardId::Oauth::AuthorizationCodeFlow,
|
|
@@ -5,6 +5,21 @@ module StandardId
|
|
|
5
5
|
|
|
6
6
|
include StandardId::PasswordlessStrategy
|
|
7
7
|
|
|
8
|
+
# RAR-60: Rate limit OTP initiation by IP (10 per hour)
|
|
9
|
+
rate_limit to: StandardId.config.rate_limits.api_passwordless_start_per_ip,
|
|
10
|
+
within: 1.hour,
|
|
11
|
+
name: "passwordless-ip",
|
|
12
|
+
only: :start,
|
|
13
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
14
|
+
|
|
15
|
+
# RAR-60: Rate limit OTP initiation by target (5 per 15 minutes)
|
|
16
|
+
rate_limit to: StandardId.config.rate_limits.api_passwordless_start_per_target,
|
|
17
|
+
within: 15.minutes,
|
|
18
|
+
by: -> { "api-passwordless:#{(params[:username] || params[:email] || params[:phone_number]).to_s.strip.downcase}" },
|
|
19
|
+
name: "passwordless-target",
|
|
20
|
+
only: :start,
|
|
21
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
22
|
+
|
|
8
23
|
def start
|
|
9
24
|
raise StandardId::InvalidRequestError, "username, email, or phone_number parameter is required" if start_params[:username].blank?
|
|
10
25
|
|
|
@@ -5,6 +5,7 @@ module StandardId
|
|
|
5
5
|
include StandardId::WebAuthentication
|
|
6
6
|
include StandardId::SetCurrentRequestDetails
|
|
7
7
|
include StandardId::WebMechanismGate
|
|
8
|
+
include StandardId::RateLimitHandling
|
|
8
9
|
|
|
9
10
|
include StandardId::WebEngine.routes.url_helpers
|
|
10
11
|
helper StandardId::WebEngine.routes.url_helpers
|
|
@@ -10,6 +10,21 @@ module StandardId
|
|
|
10
10
|
|
|
11
11
|
layout "public"
|
|
12
12
|
|
|
13
|
+
# RAR-51: Rate limit login attempts by IP (20 per 15 minutes)
|
|
14
|
+
rate_limit to: StandardId.config.rate_limits.password_login_per_ip,
|
|
15
|
+
within: 15.minutes,
|
|
16
|
+
name: "login-ip",
|
|
17
|
+
only: :create,
|
|
18
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
19
|
+
|
|
20
|
+
# RAR-51: Rate limit login attempts by email target (5 per 15 minutes)
|
|
21
|
+
rate_limit to: StandardId.config.rate_limits.password_login_per_email,
|
|
22
|
+
within: 15.minutes,
|
|
23
|
+
by: -> { "login-email:#{params.dig(:login, :email).to_s.strip.downcase}" },
|
|
24
|
+
name: "login-email",
|
|
25
|
+
only: :create,
|
|
26
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
27
|
+
|
|
13
28
|
skip_before_action :require_browser_session!, only: [:show, :create]
|
|
14
29
|
|
|
15
30
|
before_action :redirect_if_authenticated, only: [:show]
|
|
@@ -9,6 +9,13 @@ module StandardId
|
|
|
9
9
|
|
|
10
10
|
layout "public"
|
|
11
11
|
|
|
12
|
+
# RAR-60: Rate limit OTP verification attempts by IP (20 per 15 minutes)
|
|
13
|
+
rate_limit to: StandardId.config.rate_limits.otp_verify_per_ip,
|
|
14
|
+
within: 15.minutes,
|
|
15
|
+
name: "otp-verify-ip",
|
|
16
|
+
only: :update,
|
|
17
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
18
|
+
|
|
12
19
|
skip_before_action :require_browser_session!, only: [:show, :update]
|
|
13
20
|
before_action :redirect_if_authenticated, only: [:show]
|
|
14
21
|
before_action :require_otp_payload!
|
|
@@ -26,11 +33,12 @@ module StandardId
|
|
|
26
33
|
return
|
|
27
34
|
end
|
|
28
35
|
|
|
29
|
-
result = StandardId::Passwordless
|
|
30
|
-
connection: @otp_data[:connection],
|
|
36
|
+
result = StandardId::Passwordless.verify(
|
|
31
37
|
username: @otp_data[:username],
|
|
32
38
|
code: code,
|
|
33
|
-
|
|
39
|
+
connection: @otp_data[:connection],
|
|
40
|
+
request: request,
|
|
41
|
+
allow_registration: passwordless_registration_enabled?
|
|
34
42
|
)
|
|
35
43
|
|
|
36
44
|
unless result.success?
|
|
@@ -45,7 +53,10 @@ module StandardId
|
|
|
45
53
|
session_manager.sign_in_account(account)
|
|
46
54
|
emit_authentication_succeeded(account)
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
if newly_created
|
|
57
|
+
emit_passwordless_account_created(account)
|
|
58
|
+
invoke_after_account_created(account, { mechanism: "passwordless", provider: nil })
|
|
59
|
+
end
|
|
49
60
|
|
|
50
61
|
context = { connection: @otp_data[:connection], provider: nil }
|
|
51
62
|
redirect_override = invoke_after_sign_in(account, context)
|
|
@@ -81,6 +92,19 @@ module StandardId
|
|
|
81
92
|
end
|
|
82
93
|
end
|
|
83
94
|
|
|
95
|
+
def passwordless_registration_enabled?
|
|
96
|
+
StandardId.config.web.passwordless_registration
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def emit_passwordless_account_created(account)
|
|
100
|
+
StandardId::Events.publish(
|
|
101
|
+
StandardId::Events::PASSWORDLESS_ACCOUNT_CREATED,
|
|
102
|
+
account: account,
|
|
103
|
+
channel: @otp_data[:connection],
|
|
104
|
+
identifier: @otp_data[:username]
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
84
108
|
def emit_authentication_succeeded(account)
|
|
85
109
|
StandardId::Events.publish(
|
|
86
110
|
StandardId::Events::AUTHENTICATION_SUCCEEDED,
|
|
@@ -2,6 +2,21 @@ module StandardId
|
|
|
2
2
|
module Web
|
|
3
3
|
module VerifyEmail
|
|
4
4
|
class StartController < BaseController
|
|
5
|
+
# RAR-56: Rate limit verification code generation by IP (10 per hour)
|
|
6
|
+
rate_limit to: StandardId.config.rate_limits.verification_start_per_ip,
|
|
7
|
+
within: 1.hour,
|
|
8
|
+
name: "verify-email-ip",
|
|
9
|
+
only: :create,
|
|
10
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
11
|
+
|
|
12
|
+
# RAR-56: Rate limit verification code generation by email target (3 per 15 minutes)
|
|
13
|
+
rate_limit to: StandardId.config.rate_limits.verification_start_per_target,
|
|
14
|
+
within: 15.minutes,
|
|
15
|
+
by: -> { "verify-email:#{params[:email].to_s.strip.downcase}" },
|
|
16
|
+
name: "verify-email-target",
|
|
17
|
+
only: :create,
|
|
18
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
19
|
+
|
|
5
20
|
def show
|
|
6
21
|
render plain: "verify email start", status: :ok
|
|
7
22
|
end
|
|
@@ -2,6 +2,21 @@ module StandardId
|
|
|
2
2
|
module Web
|
|
3
3
|
module VerifyPhone
|
|
4
4
|
class StartController < BaseController
|
|
5
|
+
# RAR-56: Rate limit verification code generation by IP (10 per hour)
|
|
6
|
+
rate_limit to: StandardId.config.rate_limits.verification_start_per_ip,
|
|
7
|
+
within: 1.hour,
|
|
8
|
+
name: "verify-phone-ip",
|
|
9
|
+
only: :create,
|
|
10
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
11
|
+
|
|
12
|
+
# RAR-56: Rate limit verification code generation by phone target (3 per 15 minutes)
|
|
13
|
+
rate_limit to: StandardId.config.rate_limits.verification_start_per_target,
|
|
14
|
+
within: 15.minutes,
|
|
15
|
+
by: -> { "verify-phone:#{params[:phone_number].to_s.strip}" },
|
|
16
|
+
name: "verify-phone-target",
|
|
17
|
+
only: :create,
|
|
18
|
+
store: StandardId::RateLimitHandling::RATE_LIMIT_STORE
|
|
19
|
+
|
|
5
20
|
def show
|
|
6
21
|
render plain: "verify phone start", status: :ok
|
|
7
22
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
class PasswordlessMailer < ApplicationMailer
|
|
3
|
+
layout false
|
|
4
|
+
|
|
5
|
+
def otp_email
|
|
6
|
+
@otp_code = params[:otp_code]
|
|
7
|
+
@email = params[:email]
|
|
8
|
+
|
|
9
|
+
mail(
|
|
10
|
+
to: @email,
|
|
11
|
+
from: StandardId.config.passwordless.mailer_from,
|
|
12
|
+
subject: StandardId.config.passwordless.mailer_subject
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<style>
|
|
6
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f5f5f5; margin: 0; padding: 0; }
|
|
7
|
+
.container { max-width: 480px; margin: 40px auto; background-color: #ffffff; border-radius: 8px; padding: 40px; }
|
|
8
|
+
.code { font-size: 32px; font-weight: bold; letter-spacing: 8px; text-align: center; padding: 20px; background-color: #f0f0f0; border-radius: 6px; margin: 24px 0; font-family: monospace; }
|
|
9
|
+
.footer { margin-top: 32px; font-size: 13px; color: #888888; text-align: center; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div class="container">
|
|
14
|
+
<p>Hi,</p>
|
|
15
|
+
<p>Use the following code to sign in:</p>
|
|
16
|
+
<div class="code"><%= @otp_code %></div>
|
|
17
|
+
<p>This code will expire in <%= StandardId.config.passwordless.code_ttl / 60 %> minutes.</p>
|
|
18
|
+
<p>If you did not request this code, you can safely ignore this email.</p>
|
|
19
|
+
<div class="footer">
|
|
20
|
+
<p>This is an automated message. Please do not reply.</p>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Hi,
|
|
2
|
+
|
|
3
|
+
Use the following code to sign in:
|
|
4
|
+
|
|
5
|
+
<%= @otp_code %>
|
|
6
|
+
|
|
7
|
+
This code will expire in <%= StandardId.config.passwordless.code_ttl / 60 %> minutes.
|
|
8
|
+
|
|
9
|
+
If you did not request this code, you can safely ignore this email.
|
|
10
|
+
|
|
11
|
+
--
|
|
12
|
+
This is an automated message. Please do not reply.
|
|
@@ -54,6 +54,11 @@ StandardId.configure do |c|
|
|
|
54
54
|
# }
|
|
55
55
|
# }
|
|
56
56
|
# c.oauth.allowed_audiences = %w[web mobile admin] # Empty = no validation
|
|
57
|
+
#
|
|
58
|
+
# Custom claims added to every access token (independent of scopes).
|
|
59
|
+
# Receives keyword arguments: account:, client:, request:, audience:
|
|
60
|
+
# Must return a Hash. Reserved JWT keys (sub, exp, iat, etc.) are excluded.
|
|
61
|
+
# c.oauth.custom_claims = ->(account:, **) { { channel_id: account.channel_id } }
|
|
57
62
|
|
|
58
63
|
# JWT Signing Configuration (Asymmetric Algorithms)
|
|
59
64
|
# By default, JWTs are signed with HS256 using Rails.application.secret_key_base.
|
|
@@ -50,6 +50,20 @@ StandardConfig.schema.draw do
|
|
|
50
50
|
field :max_attempts, type: :integer, default: 3
|
|
51
51
|
field :retry_delay, type: :integer, default: 30 # 30 seconds
|
|
52
52
|
field :bypass_code, type: :string, default: nil # E2E testing only — NEVER set in production
|
|
53
|
+
|
|
54
|
+
# Custom account factory for passwordless registration.
|
|
55
|
+
# When set, replaces the default find_or_create_account! logic in strategies.
|
|
56
|
+
# Must be a callable (lambda/proc) that receives (identifier:, params:, request:)
|
|
57
|
+
# and returns an Account (or account-like) record.
|
|
58
|
+
# When nil (default), uses the built-in strategy behavior.
|
|
59
|
+
field :account_factory, type: :any, default: nil
|
|
60
|
+
|
|
61
|
+
# OTP email delivery mode:
|
|
62
|
+
# :custom — (default) host app handles delivery via event subscriber
|
|
63
|
+
# :built_in — engine sends OTP emails automatically using PasswordlessMailer
|
|
64
|
+
field :delivery, type: :symbol, default: :custom
|
|
65
|
+
field :mailer_from, type: :string, default: "noreply@example.com"
|
|
66
|
+
field :mailer_subject, type: :string, default: "Your sign-in code"
|
|
53
67
|
end
|
|
54
68
|
|
|
55
69
|
scope :password do
|
|
@@ -90,6 +104,12 @@ StandardConfig.schema.draw do
|
|
|
90
104
|
# Asymmetric (RSA): :rs256, :rs384, :rs512
|
|
91
105
|
# Asymmetric (ECDSA): :es256, :es384, :es512
|
|
92
106
|
field :signing_algorithm, type: :symbol, default: :hs256
|
|
107
|
+
|
|
108
|
+
# Custom claims callable for encoding additional claims into JWT access tokens.
|
|
109
|
+
# Receives keyword arguments: account:, client:, request:, audience:
|
|
110
|
+
# Must return a Hash of custom claims to merge into the JWT payload.
|
|
111
|
+
# Example: ->(account:, **) { { channel_id: account.channel_id } }
|
|
112
|
+
field :custom_claims, type: :any, default: nil
|
|
93
113
|
end
|
|
94
114
|
|
|
95
115
|
scope :social do
|
|
@@ -108,5 +128,25 @@ StandardConfig.schema.draw do
|
|
|
108
128
|
field :email_verification, type: :boolean, default: true
|
|
109
129
|
field :phone_verification, type: :boolean, default: true
|
|
110
130
|
field :sessions_management, type: :boolean, default: true
|
|
131
|
+
field :passwordless_registration, type: :boolean, default: false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Rate limiting defaults (used by Rails 8 built-in rate_limit DSL)
|
|
135
|
+
scope :rate_limits do
|
|
136
|
+
# RAR-51: Password login
|
|
137
|
+
field :password_login_per_ip, type: :integer, default: 20 # per 15 minutes
|
|
138
|
+
field :password_login_per_email, type: :integer, default: 5 # per 15 minutes
|
|
139
|
+
|
|
140
|
+
# RAR-60: OTP verification
|
|
141
|
+
field :otp_verify_per_ip, type: :integer, default: 20 # per 15 minutes
|
|
142
|
+
|
|
143
|
+
# RAR-56: Email/phone verification code generation
|
|
144
|
+
field :verification_start_per_target, type: :integer, default: 3 # per 15 minutes
|
|
145
|
+
field :verification_start_per_ip, type: :integer, default: 10 # per hour
|
|
146
|
+
|
|
147
|
+
# API equivalents
|
|
148
|
+
field :api_passwordless_start_per_ip, type: :integer, default: 10 # per hour
|
|
149
|
+
field :api_passwordless_start_per_target, type: :integer, default: 5 # per 15 minutes
|
|
150
|
+
field :api_token_per_ip, type: :integer, default: 30 # per 15 minutes
|
|
111
151
|
end
|
|
112
152
|
end
|
data/lib/standard_id/engine.rb
CHANGED
|
@@ -23,6 +23,7 @@ module StandardId
|
|
|
23
23
|
|
|
24
24
|
StandardId::Events::Subscribers::AccountStatusSubscriber.attach
|
|
25
25
|
StandardId::Events::Subscribers::AccountLockingSubscriber.attach
|
|
26
|
+
StandardId::Events::Subscribers::PasswordlessDeliverySubscriber.attach
|
|
26
27
|
|
|
27
28
|
if StandardId.config.issuer.blank?
|
|
28
29
|
Rails.logger.warn("[StandardId] No issuer configured. JWT tokens will not include or verify the 'iss' claim. " \
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Events
|
|
3
|
+
module Subscribers
|
|
4
|
+
class PasswordlessDeliverySubscriber < Base
|
|
5
|
+
subscribe_to StandardId::Events::PASSWORDLESS_CODE_GENERATED
|
|
6
|
+
|
|
7
|
+
def call(event)
|
|
8
|
+
return unless built_in_delivery?
|
|
9
|
+
return unless event[:channel] == "email"
|
|
10
|
+
|
|
11
|
+
identifier = event[:identifier]
|
|
12
|
+
code = event[:code_challenge]&.code
|
|
13
|
+
|
|
14
|
+
return if identifier.blank? || code.blank?
|
|
15
|
+
|
|
16
|
+
StandardId::PasswordlessMailer.with(
|
|
17
|
+
email: identifier,
|
|
18
|
+
otp_code: code
|
|
19
|
+
).otp_email.deliver_later
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def handle_error(error, event)
|
|
23
|
+
StandardId.logger.error(
|
|
24
|
+
"[StandardId::PasswordlessDelivery] Failed to deliver OTP email " \
|
|
25
|
+
"for #{event[:identifier]}: #{error.message}"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def built_in_delivery?
|
|
32
|
+
StandardId.config.passwordless.delivery == :built_in
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -7,7 +7,7 @@ require "digest"
|
|
|
7
7
|
module StandardId
|
|
8
8
|
class JwtService
|
|
9
9
|
RESERVED_JWT_KEYS = %i[sub client_id scope grant_type exp iat aud iss nbf jti]
|
|
10
|
-
BASE_SESSION_FIELDS = %i[account_id client_id scopes grant_type aud]
|
|
10
|
+
BASE_SESSION_FIELDS = %i[account_id client_id scopes grant_type aud claims]
|
|
11
11
|
|
|
12
12
|
# Supported signing algorithms categorized by type
|
|
13
13
|
# Symmetric: use shared secret (Rails.application.secret_key_base)
|
|
@@ -187,7 +187,8 @@ module StandardId
|
|
|
187
187
|
client_id: payload[:client_id],
|
|
188
188
|
scopes: scopes,
|
|
189
189
|
grant_type: payload[:grant_type],
|
|
190
|
-
aud: payload[:aud]
|
|
190
|
+
aud: payload[:aud],
|
|
191
|
+
claims: payload.to_h
|
|
191
192
|
)
|
|
192
193
|
end
|
|
193
194
|
|
|
@@ -239,7 +240,7 @@ module StandardId
|
|
|
239
240
|
def self.claim_resolver_keys
|
|
240
241
|
resolvers = StandardId.config.oauth.claim_resolvers
|
|
241
242
|
keys = Hash.try_convert(resolvers)&.keys
|
|
242
|
-
keys.compact.map(&:to_sym).uniq.excluding(*RESERVED_JWT_KEYS)
|
|
243
|
+
keys.compact.map(&:to_sym).uniq.excluding(*RESERVED_JWT_KEYS, *BASE_SESSION_FIELDS)
|
|
243
244
|
rescue StandardError
|
|
244
245
|
[]
|
|
245
246
|
end
|
|
@@ -62,7 +62,7 @@ module StandardId
|
|
|
62
62
|
aud: audience
|
|
63
63
|
}.compact
|
|
64
64
|
|
|
65
|
-
base_payload.merge(claims_from_scope_mapping)
|
|
65
|
+
base_payload.merge(claims_from_scope_mapping).merge(claims_from_custom_claims)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def token_expiry
|
|
@@ -74,6 +74,7 @@ module StandardId
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def generate_refresh_token
|
|
77
|
+
# custom_claims not included — refresh tokens carry identity only
|
|
77
78
|
jti = SecureRandom.uuid
|
|
78
79
|
payload = {
|
|
79
80
|
sub: subject_id,
|
|
@@ -155,6 +156,21 @@ module StandardId
|
|
|
155
156
|
end
|
|
156
157
|
end
|
|
157
158
|
|
|
159
|
+
def claims_from_custom_claims
|
|
160
|
+
callable = StandardId.config.oauth.custom_claims
|
|
161
|
+
return {} unless callable.respond_to?(:call)
|
|
162
|
+
|
|
163
|
+
result = StandardId::Utils::CallableParameterFilter.filter(callable, claim_resolvers_context)
|
|
164
|
+
claims = callable.call(**result)
|
|
165
|
+
return {} unless claims.is_a?(Hash)
|
|
166
|
+
|
|
167
|
+
# Prevent custom claims from overriding reserved JWT keys or base session fields
|
|
168
|
+
claims.symbolize_keys.except(*StandardId::JwtService::RESERVED_JWT_KEYS, *StandardId::JwtService::BASE_SESSION_FIELDS)
|
|
169
|
+
rescue StandardError => e
|
|
170
|
+
StandardId.config.logger&.error("[StandardId] custom_claims callable raised: #{e.message}")
|
|
171
|
+
{}
|
|
172
|
+
end
|
|
173
|
+
|
|
158
174
|
def claims_from_scope_mapping
|
|
159
175
|
scope_claims = StandardId.config.oauth.scope_claims.with_indifferent_access
|
|
160
176
|
resolvers = StandardId.config.oauth.claim_resolvers.with_indifferent_access
|
|
@@ -53,12 +53,37 @@ module StandardId
|
|
|
53
53
|
raise NotImplementedError
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def find_existing_account(_username)
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
end
|
|
59
|
+
|
|
56
60
|
public
|
|
57
61
|
|
|
58
|
-
# Public wrapper to reuse account lookup/creation outside OTP verification
|
|
62
|
+
# Public wrapper to reuse account lookup/creation outside OTP verification.
|
|
63
|
+
# When a custom account_factory is configured, delegates to it instead of
|
|
64
|
+
# the built-in find_or_create_account! logic.
|
|
59
65
|
def find_or_create_account(username)
|
|
60
66
|
validate_username!(username)
|
|
61
|
-
|
|
67
|
+
|
|
68
|
+
factory = StandardId.config.passwordless.account_factory
|
|
69
|
+
if factory.respond_to?(:call)
|
|
70
|
+
account = factory.call(
|
|
71
|
+
identifier: username,
|
|
72
|
+
params: request_params,
|
|
73
|
+
request: request
|
|
74
|
+
)
|
|
75
|
+
raise StandardId::InvalidRequestError, "account_factory must return an account" unless account.present?
|
|
76
|
+
account
|
|
77
|
+
else
|
|
78
|
+
find_or_create_account!(username)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Public wrapper to look up an existing account without creating one.
|
|
83
|
+
# Returns nil if no account is found for the given username.
|
|
84
|
+
def find_account(username)
|
|
85
|
+
validate_username!(username)
|
|
86
|
+
find_existing_account(username)
|
|
62
87
|
end
|
|
63
88
|
|
|
64
89
|
def identifier_class
|
|
@@ -72,6 +97,13 @@ module StandardId
|
|
|
72
97
|
|
|
73
98
|
private
|
|
74
99
|
|
|
100
|
+
# Extract request parameters safely. Returns an empty hash if the request
|
|
101
|
+
# does not support parameters (e.g. test doubles).
|
|
102
|
+
def request_params
|
|
103
|
+
return {} unless request.respond_to?(:params)
|
|
104
|
+
request.params
|
|
105
|
+
end
|
|
106
|
+
|
|
75
107
|
def emit_code_requested(username)
|
|
76
108
|
StandardId::Events.publish(
|
|
77
109
|
StandardId::Events::PASSWORDLESS_CODE_REQUESTED,
|
|
@@ -15,7 +15,14 @@ module StandardId
|
|
|
15
15
|
Account.find_or_create_by_verified_email!(email)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def find_existing_account(email)
|
|
19
|
+
normalized = email.to_s.strip.downcase
|
|
20
|
+
identifier = StandardId::EmailIdentifier.includes(:account).find_by(value: normalized)
|
|
21
|
+
identifier&.account
|
|
22
|
+
end
|
|
23
|
+
|
|
18
24
|
def sender_callback
|
|
25
|
+
return nil if StandardId.config.passwordless.delivery == :built_in
|
|
19
26
|
StandardId.config.passwordless_email_sender
|
|
20
27
|
end
|
|
21
28
|
end
|
|
@@ -21,6 +21,11 @@ module StandardId
|
|
|
21
21
|
Account.create!(identifiers_attributes:)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def find_existing_account(phone_number)
|
|
25
|
+
identifier = StandardId::PhoneNumberIdentifier.includes(:account).find_by(value: phone_number)
|
|
26
|
+
identifier&.account
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
def sender_callback
|
|
25
30
|
StandardId.config.passwordless_sms_sender
|
|
26
31
|
end
|
|
@@ -5,10 +5,13 @@ module StandardId
|
|
|
5
5
|
# - success?: true/false
|
|
6
6
|
# - account: the resolved account (nil on failure)
|
|
7
7
|
# - challenge: the consumed CodeChallenge (nil on failure)
|
|
8
|
-
# - error: error message string (nil on success)
|
|
8
|
+
# - error: human-readable error message string (nil on success)
|
|
9
|
+
# - error_code: machine-readable symbol (nil on success)
|
|
10
|
+
# One of :invalid_code, :expired, :max_attempts, :not_found, :blank_code,
|
|
11
|
+
# :account_not_found, :server_error
|
|
9
12
|
# - attempts: nil on success, 0 when no challenge was found (fabricated
|
|
10
13
|
# target), or 1+ for wrong-code failures against an active challenge
|
|
11
|
-
Result = Data.define(:success?, :account, :challenge, :error, :attempts)
|
|
14
|
+
Result = Data.define(:success?, :account, :challenge, :error, :error_code, :attempts)
|
|
12
15
|
|
|
13
16
|
STRATEGY_MAP = {
|
|
14
17
|
"email" => StandardId::Passwordless::EmailStrategy,
|
|
@@ -54,7 +57,7 @@ module StandardId
|
|
|
54
57
|
# render_error(result.error)
|
|
55
58
|
# end
|
|
56
59
|
#
|
|
57
|
-
def verify(email: nil, phone: nil, code:, request:, connection: nil, username: nil)
|
|
60
|
+
def verify(email: nil, phone: nil, code:, request:, connection: nil, username: nil, allow_registration: true)
|
|
58
61
|
# Allow callers to use connection:/username: instead of email:/phone:
|
|
59
62
|
if connection.present?
|
|
60
63
|
if username.blank?
|
|
@@ -68,31 +71,42 @@ module StandardId
|
|
|
68
71
|
end
|
|
69
72
|
end
|
|
70
73
|
|
|
71
|
-
new(email: email, phone: phone, code: code, request: request).verify
|
|
74
|
+
new(email: email, phone: phone, code: code, request: request, allow_registration: allow_registration).verify
|
|
72
75
|
end
|
|
73
76
|
end
|
|
74
77
|
|
|
75
|
-
def initialize(email: nil, phone: nil, code:, request:)
|
|
78
|
+
def initialize(email: nil, phone: nil, code:, request:, allow_registration: true)
|
|
76
79
|
@code = code.to_s.strip
|
|
77
80
|
@request = request
|
|
81
|
+
@allow_registration = allow_registration
|
|
78
82
|
resolve_target_and_channel!(email, phone)
|
|
79
83
|
end
|
|
80
84
|
|
|
81
85
|
def verify
|
|
82
86
|
if @code.blank?
|
|
83
|
-
return failure("Code is required")
|
|
87
|
+
return failure("Code is required", error_code: :blank_code)
|
|
84
88
|
end
|
|
85
89
|
|
|
86
90
|
bypass_result = try_bypass
|
|
87
91
|
return bypass_result if bypass_result
|
|
88
92
|
|
|
89
93
|
challenge = find_active_challenge
|
|
90
|
-
|
|
94
|
+
|
|
95
|
+
unless challenge.present?
|
|
96
|
+
return failure("Invalid or expired verification code", error_code: :not_found, attempts: 0)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
code_matches = secure_compare(challenge.code, @code)
|
|
91
100
|
attempts = record_failed_attempt(challenge, code_matches)
|
|
92
101
|
|
|
93
102
|
unless code_matches
|
|
94
|
-
emit_otp_validation_failed(attempts)
|
|
95
|
-
|
|
103
|
+
emit_otp_validation_failed(attempts)
|
|
104
|
+
|
|
105
|
+
if attempts >= StandardId.config.passwordless.max_attempts
|
|
106
|
+
return failure("Too many failed attempts. Please request a new code.", error_code: :max_attempts, attempts: attempts)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return failure("Invalid or expired verification code", error_code: :invalid_code, attempts: attempts)
|
|
96
110
|
end
|
|
97
111
|
|
|
98
112
|
# Re-fetch with lock inside a transaction to prevent concurrent use.
|
|
@@ -103,12 +117,18 @@ module StandardId
|
|
|
103
117
|
# No OTP_VALIDATION_FAILED event here: the code was correct but the
|
|
104
118
|
# challenge was consumed by a concurrent request — not an attacker
|
|
105
119
|
# guessing codes. Emitting a failure event would be misleading.
|
|
106
|
-
result = failure("Invalid or expired verification code", attempts: attempts)
|
|
120
|
+
result = failure("Invalid or expired verification code", error_code: :expired, attempts: attempts)
|
|
107
121
|
raise ActiveRecord::Rollback
|
|
108
122
|
end
|
|
109
123
|
|
|
110
124
|
strategy = strategy_for(@channel)
|
|
111
|
-
account = strategy
|
|
125
|
+
account = resolve_account(strategy)
|
|
126
|
+
|
|
127
|
+
unless account
|
|
128
|
+
label = @channel == "sms" ? "phone number" : "email address"
|
|
129
|
+
result = failure("No account found for this #{label}", error_code: :account_not_found)
|
|
130
|
+
raise ActiveRecord::Rollback
|
|
131
|
+
end
|
|
112
132
|
|
|
113
133
|
locked_challenge.use!
|
|
114
134
|
|
|
@@ -123,9 +143,9 @@ module StandardId
|
|
|
123
143
|
|
|
124
144
|
result
|
|
125
145
|
rescue ActiveRecord::RecordNotFound
|
|
126
|
-
failure("Invalid or expired verification code")
|
|
146
|
+
failure("Invalid or expired verification code", error_code: :expired)
|
|
127
147
|
rescue ActiveRecord::RecordInvalid => e
|
|
128
|
-
failure("Unable to complete verification: #{e.record.errors.full_messages.to_sentence}")
|
|
148
|
+
failure("Unable to complete verification: #{e.record.errors.full_messages.to_sentence}", error_code: :server_error)
|
|
129
149
|
end
|
|
130
150
|
|
|
131
151
|
private
|
|
@@ -147,7 +167,15 @@ module StandardId
|
|
|
147
167
|
return unless secure_compare(bypass_code, @code)
|
|
148
168
|
|
|
149
169
|
strategy = strategy_for(@channel)
|
|
150
|
-
account =
|
|
170
|
+
account = nil
|
|
171
|
+
ActiveRecord::Base.transaction do
|
|
172
|
+
account = resolve_account(strategy)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
unless account
|
|
176
|
+
label = @channel == "sms" ? "phone number" : "email address"
|
|
177
|
+
return failure("No account found for this #{label}", error_code: :account_not_found)
|
|
178
|
+
end
|
|
151
179
|
|
|
152
180
|
StandardId::Events.publish(
|
|
153
181
|
StandardId::Events::OTP_VALIDATED,
|
|
@@ -183,7 +211,6 @@ module StandardId
|
|
|
183
211
|
# rescued alongside account-creation errors. This is intentional — both
|
|
184
212
|
# represent unexpected persistence failures and warrant the same response.
|
|
185
213
|
def record_failed_attempt(challenge, code_matches)
|
|
186
|
-
return 0 if challenge.blank?
|
|
187
214
|
return 0 if code_matches
|
|
188
215
|
|
|
189
216
|
attempts = (challenge.metadata["attempts"] || 0) + 1
|
|
@@ -199,6 +226,17 @@ module StandardId
|
|
|
199
226
|
ActiveSupport::SecurityUtils.secure_compare(a.to_s, b.to_s)
|
|
200
227
|
end
|
|
201
228
|
|
|
229
|
+
# Resolve the account for the target identifier.
|
|
230
|
+
# When @allow_registration is true, creates a new account if none exists.
|
|
231
|
+
# When false, returns nil if no account is found.
|
|
232
|
+
def resolve_account(strategy)
|
|
233
|
+
if @allow_registration
|
|
234
|
+
strategy.find_or_create_account(@target)
|
|
235
|
+
else
|
|
236
|
+
strategy.find_account(@target)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
202
240
|
def strategy_for(channel)
|
|
203
241
|
klass = STRATEGY_MAP[channel]
|
|
204
242
|
raise StandardId::InvalidRequestError, "Unsupported connection type: #{channel}" unless klass
|
|
@@ -240,17 +278,19 @@ module StandardId
|
|
|
240
278
|
account: account,
|
|
241
279
|
challenge: challenge,
|
|
242
280
|
error: nil,
|
|
281
|
+
error_code: nil,
|
|
243
282
|
attempts: nil
|
|
244
283
|
)
|
|
245
284
|
end
|
|
246
285
|
|
|
247
286
|
# attempts is nil on success (not meaningful) and 0 when no challenge was found.
|
|
248
|
-
def failure(error, attempts: nil)
|
|
287
|
+
def failure(error, error_code: nil, attempts: nil)
|
|
249
288
|
Result.new(
|
|
250
289
|
"success?": false,
|
|
251
290
|
account: nil,
|
|
252
291
|
challenge: nil,
|
|
253
292
|
error: error,
|
|
293
|
+
error_code: error_code,
|
|
254
294
|
attempts: attempts
|
|
255
295
|
)
|
|
256
296
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "standard_id/passwordless/verification_service"
|
|
2
|
+
|
|
3
|
+
module StandardId
|
|
4
|
+
module Passwordless
|
|
5
|
+
class << self
|
|
6
|
+
# Public API for verifying a passwordless OTP code.
|
|
7
|
+
#
|
|
8
|
+
# This is the recommended entry point for host apps that need OTP
|
|
9
|
+
# verification without mounting WebEngine. It wraps
|
|
10
|
+
# VerificationService.verify with the same interface and result type.
|
|
11
|
+
#
|
|
12
|
+
# @param username [String] The identifier value (email or phone number)
|
|
13
|
+
# @param code [String] The OTP code to verify
|
|
14
|
+
# @param connection [String] Channel type ("email" or "sms")
|
|
15
|
+
# @param request [ActionDispatch::Request] The current request
|
|
16
|
+
# @return [VerificationService::Result] A result with:
|
|
17
|
+
# - success? — true when verification succeeded
|
|
18
|
+
# - account — the authenticated/created account (nil on failure)
|
|
19
|
+
# - challenge — the consumed CodeChallenge (nil on failure)
|
|
20
|
+
# - error — human-readable message (nil on success)
|
|
21
|
+
# - error_code — machine-readable symbol (nil on success):
|
|
22
|
+
# :invalid_code, :expired, :max_attempts, :not_found, :blank_code,
|
|
23
|
+
# :account_not_found, :server_error
|
|
24
|
+
# - attempts — failed attempt count (nil on success)
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# result = StandardId::Passwordless.verify(
|
|
28
|
+
# username: "user@example.com",
|
|
29
|
+
# code: "123456",
|
|
30
|
+
# connection: "email",
|
|
31
|
+
# request: request
|
|
32
|
+
# )
|
|
33
|
+
#
|
|
34
|
+
# if result.success?
|
|
35
|
+
# sign_in(result.account)
|
|
36
|
+
# else
|
|
37
|
+
# case result.error_code
|
|
38
|
+
# when :invalid_code then render_invalid_code
|
|
39
|
+
# when :expired then render_expired
|
|
40
|
+
# when :max_attempts then render_locked_out
|
|
41
|
+
# when :not_found then render_not_found
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
def verify(username:, code:, connection:, request:, allow_registration: true)
|
|
46
|
+
VerificationService.verify(
|
|
47
|
+
connection: connection,
|
|
48
|
+
username: username,
|
|
49
|
+
code: code,
|
|
50
|
+
request: request,
|
|
51
|
+
allow_registration: allow_registration
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
# A delegating cache store for rate limiting that lazily resolves the
|
|
3
|
+
# backing store at request time. This allows the engine's rate_limit
|
|
4
|
+
# declarations to work regardless of boot order, and respects the host
|
|
5
|
+
# app's cache configuration.
|
|
6
|
+
#
|
|
7
|
+
# Resolution order:
|
|
8
|
+
# 1. StandardId.config.cache_store (if it responds to :increment)
|
|
9
|
+
# 2. Rails.cache
|
|
10
|
+
class RateLimitStore
|
|
11
|
+
def increment(name, amount = 1, **options)
|
|
12
|
+
resolve_store.increment(name, amount, **options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def read(name, **options)
|
|
16
|
+
resolve_store.read(name, **options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def write(name, value, **options)
|
|
20
|
+
resolve_store.write(name, value, **options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete(name, **options)
|
|
24
|
+
resolve_store.delete(name, **options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear(**options)
|
|
28
|
+
resolve_store.clear(**options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def resolve_store
|
|
34
|
+
configured = StandardId.config.cache_store
|
|
35
|
+
if configured.respond_to?(:increment)
|
|
36
|
+
configured
|
|
37
|
+
else
|
|
38
|
+
Rails.cache
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/standard_id/version.rb
CHANGED
data/lib/standard_id.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "standard_id/events/subscribers/base"
|
|
|
10
10
|
require "standard_id/events/subscribers/logging_subscriber"
|
|
11
11
|
require "standard_id/events/subscribers/account_status_subscriber"
|
|
12
12
|
require "standard_id/events/subscribers/account_locking_subscriber"
|
|
13
|
+
require "standard_id/events/subscribers/passwordless_delivery_subscriber"
|
|
13
14
|
require "standard_id/account_status"
|
|
14
15
|
require "standard_id/account_locking"
|
|
15
16
|
require "standard_id/http_client"
|
|
@@ -40,9 +41,11 @@ require "standard_id/passwordless/base_strategy"
|
|
|
40
41
|
require "standard_id/passwordless/email_strategy"
|
|
41
42
|
require "standard_id/passwordless/sms_strategy"
|
|
42
43
|
require "standard_id/passwordless/verification_service"
|
|
44
|
+
require "standard_id/passwordless"
|
|
43
45
|
require "standard_id/authorization_bypass"
|
|
44
46
|
require "standard_id/utils/callable_parameter_filter"
|
|
45
47
|
require "standard_id/utils/ip_normalizer"
|
|
48
|
+
require "standard_id/rate_limit_store"
|
|
46
49
|
|
|
47
50
|
require "concurrent/delay"
|
|
48
51
|
|
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.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -114,6 +114,7 @@ files:
|
|
|
114
114
|
- app/controllers/concerns/standard_id/inertia_support.rb
|
|
115
115
|
- app/controllers/concerns/standard_id/lifecycle_hooks.rb
|
|
116
116
|
- app/controllers/concerns/standard_id/passwordless_strategy.rb
|
|
117
|
+
- app/controllers/concerns/standard_id/rate_limit_handling.rb
|
|
117
118
|
- app/controllers/concerns/standard_id/sentry_context.rb
|
|
118
119
|
- app/controllers/concerns/standard_id/set_current_request_details.rb
|
|
119
120
|
- app/controllers/concerns/standard_id/social_authentication.rb
|
|
@@ -156,6 +157,7 @@ files:
|
|
|
156
157
|
- app/jobs/standard_id/cleanup_expired_refresh_tokens_job.rb
|
|
157
158
|
- app/jobs/standard_id/cleanup_expired_sessions_job.rb
|
|
158
159
|
- app/mailers/standard_id/application_mailer.rb
|
|
160
|
+
- app/mailers/standard_id/passwordless_mailer.rb
|
|
159
161
|
- app/models/concerns/standard_id/account_associations.rb
|
|
160
162
|
- app/models/concerns/standard_id/credentiable.rb
|
|
161
163
|
- app/models/concerns/standard_id/password_strength.rb
|
|
@@ -175,6 +177,8 @@ files:
|
|
|
175
177
|
- app/models/standard_id/service_session.rb
|
|
176
178
|
- app/models/standard_id/session.rb
|
|
177
179
|
- app/models/standard_id/username_identifier.rb
|
|
180
|
+
- app/views/standard_id/passwordless_mailer/otp_email.html.erb
|
|
181
|
+
- app/views/standard_id/passwordless_mailer/otp_email.text.erb
|
|
178
182
|
- app/views/standard_id/web/account/edit.html.erb
|
|
179
183
|
- app/views/standard_id/web/account/show.html.erb
|
|
180
184
|
- app/views/standard_id/web/auth/callback/providers/mobile_callback.html.erb
|
|
@@ -227,6 +231,7 @@ files:
|
|
|
227
231
|
- lib/standard_id/events/subscribers/account_status_subscriber.rb
|
|
228
232
|
- lib/standard_id/events/subscribers/base.rb
|
|
229
233
|
- lib/standard_id/events/subscribers/logging_subscriber.rb
|
|
234
|
+
- lib/standard_id/events/subscribers/passwordless_delivery_subscriber.rb
|
|
230
235
|
- lib/standard_id/http_client.rb
|
|
231
236
|
- lib/standard_id/jwt_service.rb
|
|
232
237
|
- lib/standard_id/oauth/authorization_code_authorization_flow.rb
|
|
@@ -244,12 +249,14 @@ files:
|
|
|
244
249
|
- lib/standard_id/oauth/subflows/traditional_code_grant.rb
|
|
245
250
|
- lib/standard_id/oauth/token_grant_flow.rb
|
|
246
251
|
- lib/standard_id/oauth/token_lifetime_resolver.rb
|
|
252
|
+
- lib/standard_id/passwordless.rb
|
|
247
253
|
- lib/standard_id/passwordless/base_strategy.rb
|
|
248
254
|
- lib/standard_id/passwordless/email_strategy.rb
|
|
249
255
|
- lib/standard_id/passwordless/sms_strategy.rb
|
|
250
256
|
- lib/standard_id/passwordless/verification_service.rb
|
|
251
257
|
- lib/standard_id/provider_registry.rb
|
|
252
258
|
- lib/standard_id/providers/base.rb
|
|
259
|
+
- lib/standard_id/rate_limit_store.rb
|
|
253
260
|
- lib/standard_id/testing.rb
|
|
254
261
|
- lib/standard_id/testing/authentication_helpers.rb
|
|
255
262
|
- lib/standard_id/testing/factories/credentials.rb
|