standard_id 0.54.1 → 0.55.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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +123 -0
  4. data/app/controllers/standard_id/api/email_password_controller.rb +131 -0
  5. data/app/forms/standard_id/web/reset_password_confirm_form.rb +6 -7
  6. data/app/jobs/standard_id/purge_unverified_email_signups_job.rb +141 -0
  7. data/app/mailers/standard_id/account_notice_mailer.rb +33 -0
  8. data/app/models/standard_id/identifier.rb +22 -2
  9. data/app/models/standard_id/session.rb +4 -4
  10. data/app/views/standard_id/account_notice_mailer/existing_account.html.erb +22 -0
  11. data/app/views/standard_id/account_notice_mailer/existing_account.text.erb +10 -0
  12. data/config/locales/en.yml +8 -0
  13. data/config/routes/api.rb +14 -0
  14. data/db/migrate/20261022000000_add_signup_source_to_standard_id_identifiers.rb +13 -0
  15. data/lib/standard_id/access_revocation.rb +40 -0
  16. data/lib/standard_id/config/callable_validator.rb +49 -0
  17. data/lib/standard_id/config/schema.rb +108 -0
  18. data/lib/standard_id/email_password/authentication.rb +71 -0
  19. data/lib/standard_id/email_password/daily_limit.rb +67 -0
  20. data/lib/standard_id/email_password/password_reset.rb +148 -0
  21. data/lib/standard_id/email_password/registration.rb +141 -0
  22. data/lib/standard_id/email_password/token_flow.rb +36 -0
  23. data/lib/standard_id/email_password/verification.rb +155 -0
  24. data/lib/standard_id/email_password.rb +209 -0
  25. data/lib/standard_id/errors.rb +39 -0
  26. data/lib/standard_id/login_method_policy.rb +2 -1
  27. data/lib/standard_id/version.rb +1 -1
  28. data/lib/standard_id.rb +8 -0
  29. metadata +15 -1
@@ -0,0 +1,36 @@
1
+ module StandardId
2
+ module EmailPassword
3
+ # Mints the token response for an account that Authentication or
4
+ # Verification has just proven, through the same pipeline as every other
5
+ # grant: login-method policy (flow :api_email_password), audience checks,
6
+ # claims, OAUTH_TOKEN_ISSUING / OAUTH_TOKEN_ISSUED (so AccountStatus and
7
+ # AccountLocking refuse), refresh token and session persistence.
8
+ #
9
+ # client_id, scope and audience come from `c.email_password.token_*`,
10
+ # never from the request. Not reachable from /oauth/token.
11
+ class TokenFlow < StandardId::Oauth::TokenGrantFlow
12
+ def initialize(account:, request:)
13
+ super({}, request)
14
+ @account = account
15
+ end
16
+
17
+ def authenticate!
18
+ raise StandardId::InvalidGrantError, "Account is required" if @account.blank?
19
+ end
20
+
21
+ private
22
+
23
+ def login_method_policy_context
24
+ { auth_method: :password, flow: :api_email_password }
25
+ end
26
+
27
+ def subject_id = @account.id
28
+ def client_id = EmailPassword.config.token_client_id
29
+ def token_scope = EmailPassword.config.token_scope
30
+ def audience = EmailPassword.config.token_audience
31
+ def grant_type = "password"
32
+ def supports_refresh_token? = true
33
+ def token_account = @account
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,155 @@
1
+ module StandardId
2
+ module EmailPassword
3
+ # The sign-up code: issuing it (sign-up and resend) and confirming it.
4
+ module Verification
5
+ INVALID_CODE_MESSAGE = "Invalid or expired verification code."
6
+ TOO_MANY_ATTEMPTS_MESSAGE = "Too many incorrect attempts. Please request a new code."
7
+ LOCKED_OUT_MESSAGE = "Too many incorrect attempts. Please try again tomorrow."
8
+
9
+ class << self
10
+ # Confirms the code for a pending sign-up, verifies the address and runs
11
+ # `c.email_password.after_email_verified` when the sign-up was made
12
+ # through Registration (EmailPassword.api_signup?), as a reset does: an
13
+ # unverified web sign-up that verifies here is verified without it.
14
+ #
15
+ # Requires the sign-up password as well as the code. Code alone would be
16
+ # a takeover: anyone could start a sign-up for an address and the
17
+ # owner's code would then confirm an account the stranger controls. The
18
+ # password is checked FIRST and a mismatch is answered exactly like a
19
+ # wrong code, so a guessed code is never even compared without it.
20
+ #
21
+ # A wrong code sent WITH the right password counts toward
22
+ # rate_limits.email_password_failed_verifications_per_day; at the cap the
23
+ # address cannot verify, even with the right code, until the window
24
+ # passes. A wrong password does not count, so a stranger cannot burn the
25
+ # registrant's daily allowance, and only the right password is told the
26
+ # address is locked out (otherwise the lockout would reveal that the
27
+ # address has a pending sign-up). The controller's short per-email
28
+ # limit (rate_limits.email_password_verify_per_email) does count every
29
+ # request, so a stranger CAN hold verification off for that window.
30
+ #
31
+ # Given a block, yields the account with the password credential's row
32
+ # locked, after re-checking that the password has not changed since it
33
+ # was checked, as Authentication.call does: mint tokens inside it, so a
34
+ # password reset racing this verification either finishes first (and
35
+ # this answers :invalid_code) or revokes what it minted.
36
+ #
37
+ # @return [Account] ready for EmailPassword::TokenFlow
38
+ # @raise [StandardId::EmailPasswordError] :invalid_code
39
+ def verify!(email:, code:, password:, request: nil)
40
+ email = EmailPassword.normalize_email(email)
41
+ password_credential = EmailPassword.credential_for_password(email, password)
42
+ identifier = password_credential&.credential&.identifier
43
+ raise invalid_code(INVALID_CODE_MESSAGE) if identifier.nil?
44
+ raise invalid_code(LOCKED_OUT_MESSAGE) if DailyLimit.exceeded?(:failed_verification, email)
45
+
46
+ verified_digest = password_credential.password_digest
47
+
48
+ otp = StandardId::Otp.verify(
49
+ realm: EmailPassword.config.verification_realm,
50
+ target: email,
51
+ channel: :email,
52
+ code: code.to_s.strip,
53
+ request: request
54
+ )
55
+ failed!(email, otp.error_code) unless otp.success?
56
+
57
+ # The hook runs in the same transaction as the verification: if it
58
+ # raises, the address stays unverified, so the registrant can ask for
59
+ # a new code (this one is spent) and the hook runs again. Otherwise a
60
+ # failed hook would leave a verified account that login lets in
61
+ # without ever provisioning it.
62
+ account = ActiveRecord::Base.transaction do
63
+ password_unchanged!(password_credential, verified_digest)
64
+ EmailPassword.complete_verification!(
65
+ identifier,
66
+ name: otp.challenge&.metadata&.dig("name"),
67
+ request: request,
68
+ run_hook: EmailPassword.api_signup?(identifier)
69
+ )
70
+ identifier.account
71
+ end
72
+ return account unless block_given?
73
+
74
+ password_credential.with_lock do
75
+ password_unchanged!(password_credential, verified_digest)
76
+ yield account
77
+ end
78
+ end
79
+
80
+ # Resend: issues a fresh code for a pending sign-up, only when +password+
81
+ # is its password. Otherwise a silent no-op: the endpoint answers the
82
+ # same either way, so it cannot probe which addresses have accounts, and
83
+ # a stranger cannot spend the registrant's daily code allowance.
84
+ #
85
+ # @return [Boolean] whether a code was issued
86
+ def send_code(email:, password:, name: nil, request: nil)
87
+ email = EmailPassword.normalize_email(email)
88
+ return false unless EmailPassword.pending_identifier(email, password)
89
+
90
+ issue_code(email, name: name, request: request)
91
+ end
92
+
93
+ # Issues and delivers a code for +email+ without checking for a pending
94
+ # sign-up (Registration has just done so). At most
95
+ # rate_limits.email_password_codes_per_day per address; past that, and
96
+ # while passwordless.retry_delay is still running (the code already
97
+ # sent stays valid), a silent no-op.
98
+ #
99
+ # The name given at sign-up rides on the challenge's metadata until the
100
+ # code is confirmed; a resend carries it forward.
101
+ #
102
+ # @return [Boolean] whether a code was issued
103
+ def issue_code(email, name: nil, request: nil)
104
+ return false if DailyLimit.exceeded?(:code_issued, email)
105
+
106
+ config = EmailPassword.config
107
+ built_in = config.code_delivery.to_sym == :built_in
108
+ result = StandardId::Otp.issue(
109
+ realm: config.verification_realm,
110
+ target: email,
111
+ channel: :email,
112
+ request: request,
113
+ delivery: built_in ? :manual : :custom,
114
+ metadata: { "name" => name.presence || EmailPassword.signup_name(email) }.compact
115
+ )
116
+ return false unless result.success?
117
+
118
+ DailyLimit.record!(:code_issued, email)
119
+ if built_in
120
+ StandardId::PasswordlessMailer
121
+ .with(email: email, otp_code: result.code, realm: config.verification_realm)
122
+ .verification_email
123
+ .deliver_later
124
+ end
125
+ true
126
+ end
127
+
128
+ private
129
+
130
+ # Locks the credential (in the caller's transaction) and refuses the
131
+ # verification if a reset changed the password after it was checked.
132
+ def password_unchanged!(password_credential, verified_digest)
133
+ password_credential.lock!
134
+ raise invalid_code(INVALID_CODE_MESSAGE) unless password_credential.password_digest == verified_digest
135
+ end
136
+
137
+ def failed!(email, error_code)
138
+ count = DailyLimit.record!(:failed_verification, email)
139
+ message = if count >= DailyLimit.limit(:failed_verification)
140
+ LOCKED_OUT_MESSAGE
141
+ elsif error_code == :max_attempts
142
+ TOO_MANY_ATTEMPTS_MESSAGE
143
+ else
144
+ INVALID_CODE_MESSAGE
145
+ end
146
+ raise invalid_code(message)
147
+ end
148
+
149
+ def invalid_code(message)
150
+ StandardId::EmailPasswordError.new(:invalid_code, message)
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,209 @@
1
+ require "concurrent/delay"
2
+
3
+ module StandardId
4
+ # Email + password sign-up and sign-in for JSON clients
5
+ # (Api::EmailPasswordController), switched on with
6
+ # `c.email_password.enabled = true`.
7
+ #
8
+ # Built on the engine's primitives rather than beside them: PasswordCredential
9
+ # (has_secure_password, PasswordStrength, the single-use password_reset token),
10
+ # StandardId::Otp for the 6-digit sign-up code, PasswordResetDeliveryJob for
11
+ # forgot-password, and a TokenGrantFlow (EmailPassword::TokenFlow) for tokens,
12
+ # so a sign-in here returns the same TokenResponse as the other grants and
13
+ # refreshes through /oauth/token.
14
+ #
15
+ # The rules:
16
+ # * Sign-up creates an UNVERIFIED account and emails a code. Tokens are
17
+ # issued only once the code is confirmed, together with the sign-up
18
+ # password: the code proves the inbox, the password proves the caller is
19
+ # whoever started this sign-up.
20
+ # * Login with the right password on an unverified address answers
21
+ # :unverified, so the client can send the user to its verify screen.
22
+ # Every other login failure is one generic :invalid_credentials.
23
+ # * Re-submitting sign-up for a pending address never changes its password.
24
+ # * Resend and forgot-password always answer the same, whatever the address.
25
+ # * A reset link sets the password, verifies the address (following the
26
+ # link proves the inbox) and revokes every session and refresh token.
27
+ #
28
+ # Each operation returns its result and raises StandardId::EmailPasswordError
29
+ # otherwise, so a host can drive them from its own controllers too.
30
+ module EmailPassword
31
+ # A digest to compare against when no credential exists, so a miss costs
32
+ # the same bcrypt work as a hit and timing does not reveal whether the
33
+ # address is registered.
34
+ DUMMY_PASSWORD_DIGEST = Concurrent::Delay.new { BCrypt::Password.create("").freeze }
35
+
36
+ # Identifier#signup_source of a sign-up made through Registration. A reset
37
+ # runs `after_email_verified` only for these, and
38
+ # PurgeUnverifiedEmailSignupsJob only deletes these.
39
+ SIGNUP_SOURCE = "email_password"
40
+
41
+ class << self
42
+ def config
43
+ StandardId.config.email_password
44
+ end
45
+
46
+ def enabled?
47
+ config.enabled == true
48
+ end
49
+
50
+ def normalize_email(email)
51
+ email.to_s.strip.downcase
52
+ end
53
+
54
+ def valid_email?(email)
55
+ StandardId::EmailIdentifier::EMAIL_FORMAT.match?(email.to_s)
56
+ end
57
+
58
+ # The password credential whose login is +email+, with the identifier and
59
+ # account it hangs off preloaded.
60
+ def find_password_credential(email)
61
+ StandardId::PasswordCredential
62
+ .includes(credential: { identifier: :account })
63
+ .find_by(login: normalize_email(email))
64
+ end
65
+
66
+ # The pending sign-up for +email+ when +password+ is its password: the
67
+ # identifier of an unverified password credential, else nil. A miss still
68
+ # pays for a bcrypt comparison.
69
+ def pending_identifier(email, password)
70
+ identifier = identifier_for_password(email, password)
71
+ identifier unless identifier.nil? || identifier.verified?
72
+ end
73
+
74
+ # The identifier of the password credential for +email+, only when
75
+ # +password+ is that credential's password.
76
+ def identifier_for_password(email, password)
77
+ credential_for_password(email, password)&.credential&.identifier
78
+ end
79
+
80
+ # The password credential for +email+, only when +password+ is its
81
+ # password. A miss still pays for a bcrypt comparison.
82
+ def credential_for_password(email, password)
83
+ password = password.to_s
84
+ password_credential = find_password_credential(email)
85
+ unless password_credential
86
+ BCrypt::Password.new(DUMMY_PASSWORD_DIGEST.value).is_password?(password)
87
+ return nil
88
+ end
89
+ return nil if password.empty? || !password_credential.authenticate(password)
90
+
91
+ password_credential
92
+ end
93
+
94
+ # The first verification of an address, by code or by reset link: marks
95
+ # it verified and, for a sign-up made through Registration (+run_hook+),
96
+ # runs `c.email_password.after_email_verified`. A no-op when the address
97
+ # is already verified, so the hook runs once.
98
+ #
99
+ # Call it inside a transaction. If the hook raises, the address stays
100
+ # unverified and the next verification retries it; and
101
+ # IDENTIFIER_VERIFICATION_SUCCEEDED waits for the commit, so subscribers
102
+ # never see a verification that was rolled back.
103
+ def complete_verification!(identifier, name: nil, request: nil, run_hook: true)
104
+ identifier.lock!
105
+ return if identifier.verified?
106
+
107
+ identifier.verify!(defer_event: true)
108
+ hook = config.after_email_verified
109
+ return unless run_hook && hook.respond_to?(:call)
110
+
111
+ call_hook(hook, account: identifier.account, name: name, request: request)
112
+ end
113
+
114
+ # true when +identifier+ was created by Registration (its signup_source),
115
+ # as opposed to Web::SignupForm, a social sign-in or the host's own flow.
116
+ def api_signup?(identifier)
117
+ identifier.signup_source == SIGNUP_SOURCE
118
+ end
119
+
120
+ # Registration writes Identifier#signup_source, which migration
121
+ # 20261022000000 adds; without it a sign-up would fail with an unknown
122
+ # attribute, so say what to run instead.
123
+ def require_signup_source_column!
124
+ return if StandardId::Identifier.column_names.include?("signup_source")
125
+
126
+ raise StandardId::ConfigurationError,
127
+ "c.email_password needs migration 20261022000000_add_signup_source_to_standard_id_identifiers: " \
128
+ "run bin/rails standard_id:install:migrations && bin/rails db:migrate"
129
+ end
130
+
131
+ # The name given at sign-up for +email+, carried on its latest
132
+ # verification code; nil once CleanupExpiredCodeChallengesJob has removed
133
+ # that code.
134
+ def signup_name(email)
135
+ StandardId::CodeChallenge
136
+ .where(realm: config.verification_realm, channel: "email", target: normalize_email(email))
137
+ .order(created_at: :desc)
138
+ .pick(:metadata)
139
+ &.dig("name")
140
+ end
141
+
142
+ # Field messages for +password+ (empty when acceptable): PasswordStrength
143
+ # and has_secure_password's own checks on an unsaved probe, then
144
+ # `c.email_password.password_validator`, only once those pass.
145
+ def password_errors(password, email: nil)
146
+ probe = StandardId::PasswordCredential.new(login: email.presence || "probe@example.invalid", password: password)
147
+ probe.validate
148
+ errors = probe.errors.messages_for(:password)
149
+ return errors if errors.any?
150
+
151
+ validator = config.password_validator
152
+ return [] unless validator.respond_to?(:call)
153
+
154
+ Array(call_hook(validator, password: password, email: email)).compact.map(&:to_s)
155
+ end
156
+
157
+ # The key per-IP limits count under (`c.email_password.client_key`).
158
+ def client_key(request)
159
+ hook = config.client_key
160
+ key = hook.respond_to?(:call) ? call_hook(hook, request: request) : nil
161
+ key.presence || request&.remote_ip.to_s
162
+ end
163
+
164
+ # Pre-account-takeover defence for hosts that link social sign-ins by
165
+ # email (c.social.link_strategy = :trust_provider). If someone started a
166
+ # sign-up for an address they do not own and never confirmed it, the
167
+ # owner signing in with a provider would land in THAT account with the
168
+ # stranger's password attached. The password was never usable (login
169
+ # needs the code, which only reaches the owner), but it is one reset away
170
+ # from becoming so. Drop it: the provider sign-in proved the address, the
171
+ # pending sign-up proved nothing.
172
+ #
173
+ # When anything was discarded, every session and refresh token the
174
+ # account already held is revoked as well: whatever was issued before
175
+ # the owner proved the address was issued to someone who had not.
176
+ # SOCIAL_ACCOUNT_LINKED fires after the social flow has issued its own
177
+ # session or tokens, so pass +issued_before+ (when that login started) to
178
+ # spare them. Call it from a SOCIAL_ACCOUNT_LINKED subscriber.
179
+ #
180
+ # @return [Integer] the number of pending credentials discarded
181
+ def discard_pending_credentials!(account, issued_before: nil)
182
+ return 0 if account.nil?
183
+
184
+ discarded = 0
185
+ StandardId::Credential
186
+ .joins(:identifier)
187
+ .where(standard_id_identifiers: { account_id: account.id, verified_at: nil })
188
+ .where(credentialable_type: StandardId::PasswordCredential.name)
189
+ .find_each do |credential|
190
+ ActiveRecord::Base.transaction do
191
+ password_credential = StandardId::PasswordCredential.find_by(id: credential.credentialable_id)
192
+ credential.destroy!
193
+ password_credential&.destroy!
194
+ end
195
+ discarded += 1
196
+ end
197
+
198
+ if discarded.positive?
199
+ StandardId::AccessRevocation.revoke_all!(account, reason: "pending_signup_discarded", issued_before: issued_before)
200
+ end
201
+ discarded
202
+ end
203
+
204
+ def call_hook(hook, **context)
205
+ hook.call(**StandardId::Utils::CallableParameterFilter.filter(hook, context))
206
+ end
207
+ end
208
+ end
209
+ end
@@ -342,6 +342,45 @@ module StandardId
342
342
  class InvalidAlgorithmError < InvalidTokenError; end
343
343
  class InvalidAudienceTokenError < InvalidTokenError; end
344
344
 
345
+ # Raised by the API email + password endpoints (StandardId::EmailPassword).
346
+ # +code+ is one of CODES — stable symbols a client can branch on — and the
347
+ # status is fixed per code, so `c.email_password.error_renderer` can change
348
+ # the body without having to know the status. +details+ carries field
349
+ # messages for :weak_password and :invalid_request.
350
+ class EmailPasswordError < StandardError
351
+ CODES = {
352
+ invalid_credentials: { status: :unauthorized, message: "Invalid email or password" },
353
+ unverified: { status: :forbidden, message: "The email address has not been verified" },
354
+ account_exists: { status: :conflict, message: "An account with this email already exists" },
355
+ social_account: { status: :conflict, message: "This email is registered with a different sign-in method" },
356
+ invalid_code: { status: :unprocessable_content, message: "Invalid or expired verification code" },
357
+ invalid_reset_token: { status: :unprocessable_content, message: "The password reset link is invalid or has expired" },
358
+ weak_password: { status: :unprocessable_content, message: "The password does not meet the requirements" },
359
+ invalid_request: { status: :unprocessable_content, message: "The request is invalid" },
360
+ login_method_denied: { status: :forbidden, message: "This sign-in method is not available for this account" },
361
+ rate_limited: { status: :too_many_requests, message: "Too many requests. Please try again later." }
362
+ }.freeze
363
+
364
+ attr_reader :code, :details, :retry_after
365
+
366
+ def initialize(code, message = nil, details: {}, retry_after: nil)
367
+ @code = code.to_sym
368
+ raise ArgumentError, "Unknown EmailPasswordError code: #{code.inspect}" unless CODES.key?(@code)
369
+
370
+ @details = details.to_h
371
+ @retry_after = retry_after
372
+ super(message.presence || CODES.dig(@code, :message))
373
+ end
374
+
375
+ def status
376
+ CODES.dig(code, :status)
377
+ end
378
+
379
+ def http_status
380
+ Rack::Utils.status_code(status)
381
+ end
382
+ end
383
+
345
384
  # Raised for engine misconfiguration — a required config value is missing,
346
385
  # malformed, points at a non-callable with the wrong arity, or references a
347
386
  # scope whose claims have no resolver registered. Always indicates a
@@ -19,6 +19,7 @@ module StandardId
19
19
  # | :oauth_refresh_token | the ORIGINAL sign-in's, else :unspecified | POST /oauth/token grant_type=refresh_token |
20
20
  # | :api_device_session | caller's, else :unspecified | Api::TokenManager#create_device_session (host code) |
21
21
  # | :api_service_session | caller's, else :unspecified | Api::TokenManager#create_service_session (host code) |
22
+ # | :api_email_password | :password | POST /auth/login and /auth/verify-email (Api::EmailPasswordController) |
22
23
  #
23
24
  # `refresh_token` is re-checked with the method of the authentication it
24
25
  # descends from (see StandardId::AuthLineage): a policy tightened after a
@@ -53,7 +54,7 @@ module StandardId
53
54
  FLOWS = %i[
54
55
  web_password web_signup web_passwordless web_social web_remember_me web_session
55
56
  oauth_password_grant oauth_passwordless_otp_grant oauth_social_callback oauth_refresh_token
56
- api_device_session api_service_session
57
+ api_device_session api_service_session api_email_password
57
58
  ].freeze
58
59
 
59
60
  module_function
@@ -1,3 +1,3 @@
1
1
  module StandardId
2
- VERSION = "0.54.1"
2
+ VERSION = "0.55.0"
3
3
  end
data/lib/standard_id.rb CHANGED
@@ -77,6 +77,14 @@ require "standard_id/authorization_bypass"
77
77
  require "standard_id/utils/ip_normalizer"
78
78
  require "standard_id/rate_limit_store"
79
79
  require "standard_id/rate_limiter"
80
+ require "standard_id/access_revocation"
81
+ require "standard_id/email_password"
82
+ require "standard_id/email_password/daily_limit"
83
+ require "standard_id/email_password/verification"
84
+ require "standard_id/email_password/registration"
85
+ require "standard_id/email_password/authentication"
86
+ require "standard_id/email_password/password_reset"
87
+ require "standard_id/email_password/token_flow"
80
88
 
81
89
  require "concurrent/delay"
82
90
 
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.54.1
4
+ version: 0.55.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim
@@ -132,6 +132,7 @@ files:
132
132
  - app/controllers/concerns/standard_id/web_mechanism_gate.rb
133
133
  - app/controllers/standard_id/api/authorization_controller.rb
134
134
  - app/controllers/standard_id/api/base_controller.rb
135
+ - app/controllers/standard_id/api/email_password_controller.rb
135
136
  - app/controllers/standard_id/api/oauth/base_controller.rb
136
137
  - app/controllers/standard_id/api/oauth/callback/providers_controller.rb
137
138
  - app/controllers/standard_id/api/oauth/introspections_controller.rb
@@ -175,7 +176,9 @@ files:
175
176
  - app/jobs/standard_id/cleanup_expired_sessions_job.rb
176
177
  - app/jobs/standard_id/password_reset_delivery_job.rb
177
178
  - app/jobs/standard_id/purge_stale_dynamic_clients_job.rb
179
+ - app/jobs/standard_id/purge_unverified_email_signups_job.rb
178
180
  - app/jobs/standard_id/secure_mail_delivery_job.rb
181
+ - app/mailers/standard_id/account_notice_mailer.rb
179
182
  - app/mailers/standard_id/application_mailer.rb
180
183
  - app/mailers/standard_id/password_reset_mailer.rb
181
184
  - app/mailers/standard_id/passwordless_mailer.rb
@@ -200,6 +203,8 @@ files:
200
203
  - app/models/standard_id/session.rb
201
204
  - app/models/standard_id/social_identity.rb
202
205
  - app/models/standard_id/username_identifier.rb
206
+ - app/views/standard_id/account_notice_mailer/existing_account.html.erb
207
+ - app/views/standard_id/account_notice_mailer/existing_account.text.erb
203
208
  - app/views/standard_id/password_reset_mailer/reset_email.html.erb
204
209
  - app/views/standard_id/password_reset_mailer/reset_email.text.erb
205
210
  - app/views/standard_id/passwordless_mailer/otp_email.html.erb
@@ -243,10 +248,12 @@ files:
243
248
  - db/migrate/20261004000000_add_auth_method_to_standard_id_refresh_tokens.rb
244
249
  - db/migrate/20261020000000_add_client_application_id_to_standard_id_refresh_tokens.rb
245
250
  - db/migrate/20261020120000_add_registration_access_token_digest_to_standard_id_client_applications.rb
251
+ - db/migrate/20261022000000_add_signup_source_to_standard_id_identifiers.rb
246
252
  - lib/generators/standard_id/install/install_generator.rb
247
253
  - lib/generators/standard_id/install/templates/standard_id.rb
248
254
  - lib/generators/standard_id/install/templates/standard_id_reference.rb
249
255
  - lib/standard_id.rb
256
+ - lib/standard_id/access_revocation.rb
250
257
  - lib/standard_id/account_cleanup.rb
251
258
  - lib/standard_id/account_locking.rb
252
259
  - lib/standard_id/account_status.rb
@@ -267,6 +274,13 @@ files:
267
274
  - lib/standard_id/config_schema.rb
268
275
  - lib/standard_id/current_attributes.rb
269
276
  - lib/standard_id/deprecator.rb
277
+ - lib/standard_id/email_password.rb
278
+ - lib/standard_id/email_password/authentication.rb
279
+ - lib/standard_id/email_password/daily_limit.rb
280
+ - lib/standard_id/email_password/password_reset.rb
281
+ - lib/standard_id/email_password/registration.rb
282
+ - lib/standard_id/email_password/token_flow.rb
283
+ - lib/standard_id/email_password/verification.rb
270
284
  - lib/standard_id/engine.rb
271
285
  - lib/standard_id/errors.rb
272
286
  - lib/standard_id/events.rb