better_auth 0.8.0 → 0.11.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +142 -95
  5. data/lib/better_auth/adapters/base.rb +64 -1
  6. data/lib/better_auth/adapters/internal_adapter.rb +197 -11
  7. data/lib/better_auth/adapters/join_support.rb +10 -0
  8. data/lib/better_auth/adapters/memory.rb +303 -54
  9. data/lib/better_auth/adapters/mssql.rb +7 -3
  10. data/lib/better_auth/adapters/sql.rb +390 -63
  11. data/lib/better_auth/api.rb +3 -24
  12. data/lib/better_auth/configuration.rb +48 -47
  13. data/lib/better_auth/context.rb +43 -67
  14. data/lib/better_auth/cookies.rb +119 -15
  15. data/lib/better_auth/core.rb +0 -2
  16. data/lib/better_auth/crypto.rb +4 -3
  17. data/lib/better_auth/doctor.rb +108 -0
  18. data/lib/better_auth/endpoint.rb +88 -5
  19. data/lib/better_auth/error.rb +3 -1
  20. data/lib/better_auth/http_client.rb +46 -0
  21. data/lib/better_auth/logger.rb +1 -1
  22. data/lib/better_auth/middleware/origin_check.rb +9 -1
  23. data/lib/better_auth/migration_plan.rb +15 -0
  24. data/lib/better_auth/oauth2.rb +1 -1
  25. data/lib/better_auth/oauth_state.rb +147 -0
  26. data/lib/better_auth/plugin_loader.rb +220 -0
  27. data/lib/better_auth/plugins/access.rb +10 -13
  28. data/lib/better_auth/plugins/admin.rb +85 -29
  29. data/lib/better_auth/plugins/anonymous.rb +2 -0
  30. data/lib/better_auth/plugins/api_key.rb +4 -7
  31. data/lib/better_auth/plugins/bearer.rb +8 -22
  32. data/lib/better_auth/plugins/captcha.rb +24 -6
  33. data/lib/better_auth/plugins/device_authorization.rb +77 -5
  34. data/lib/better_auth/plugins/dub.rb +10 -2
  35. data/lib/better_auth/plugins/email_otp.rb +30 -47
  36. data/lib/better_auth/plugins/generic_oauth.rb +162 -140
  37. data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
  38. data/lib/better_auth/plugins/i18n.rb +215 -0
  39. data/lib/better_auth/plugins/jwt.rb +44 -19
  40. data/lib/better_auth/plugins/magic_link.rb +18 -21
  41. data/lib/better_auth/plugins/oauth_popup.rb +330 -0
  42. data/lib/better_auth/plugins/oauth_protocol.rb +233 -46
  43. data/lib/better_auth/plugins/oauth_provider.rb +4 -7
  44. data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
  45. data/lib/better_auth/plugins/one_tap.rb +78 -65
  46. data/lib/better_auth/plugins/one_time_token.rb +1 -4
  47. data/lib/better_auth/plugins/open_api.rb +77 -9
  48. data/lib/better_auth/plugins/organization.rb +546 -102
  49. data/lib/better_auth/plugins/passkey.rb +4 -7
  50. data/lib/better_auth/plugins/phone_number.rb +25 -27
  51. data/lib/better_auth/plugins/scim.rb +4 -7
  52. data/lib/better_auth/plugins/siwe.rb +126 -16
  53. data/lib/better_auth/plugins/sso.rb +4 -7
  54. data/lib/better_auth/plugins/stripe.rb +4 -7
  55. data/lib/better_auth/plugins/two_factor.rb +205 -19
  56. data/lib/better_auth/plugins/username.rb +56 -13
  57. data/lib/better_auth/plugins.rb +57 -0
  58. data/lib/better_auth/rate_limiter.rb +300 -133
  59. data/lib/better_auth/request_ip.rb +123 -13
  60. data/lib/better_auth/router.rb +3 -3
  61. data/lib/better_auth/routes/account.rb +44 -8
  62. data/lib/better_auth/routes/email_verification.rb +93 -6
  63. data/lib/better_auth/routes/password.rb +6 -6
  64. data/lib/better_auth/routes/session.rb +7 -2
  65. data/lib/better_auth/routes/sign_in.rb +10 -1
  66. data/lib/better_auth/routes/sign_up.rb +45 -22
  67. data/lib/better_auth/routes/social.rb +197 -54
  68. data/lib/better_auth/routes/user.rb +17 -10
  69. data/lib/better_auth/schema/sql.rb +189 -34
  70. data/lib/better_auth/schema.rb +159 -5
  71. data/lib/better_auth/session.rb +29 -10
  72. data/lib/better_auth/session_store.rb +1 -1
  73. data/lib/better_auth/social_providers/apple.rb +6 -1
  74. data/lib/better_auth/social_providers/atlassian.rb +1 -0
  75. data/lib/better_auth/social_providers/base.rb +45 -23
  76. data/lib/better_auth/social_providers/cognito.rb +75 -5
  77. data/lib/better_auth/social_providers/facebook.rb +95 -1
  78. data/lib/better_auth/social_providers/figma.rb +2 -0
  79. data/lib/better_auth/social_providers/github.rb +2 -0
  80. data/lib/better_auth/social_providers/google.rb +12 -1
  81. data/lib/better_auth/social_providers/line.rb +35 -1
  82. data/lib/better_auth/social_providers/linear.rb +1 -0
  83. data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
  84. data/lib/better_auth/social_providers/naver.rb +24 -1
  85. data/lib/better_auth/social_providers/notion.rb +1 -0
  86. data/lib/better_auth/social_providers/paybin.rb +25 -1
  87. data/lib/better_auth/social_providers/paypal.rb +118 -2
  88. data/lib/better_auth/social_providers/reddit.rb +25 -4
  89. data/lib/better_auth/social_providers/salesforce.rb +1 -0
  90. data/lib/better_auth/social_providers/twitch.rb +24 -1
  91. data/lib/better_auth/social_providers/twitter.rb +36 -1
  92. data/lib/better_auth/social_providers/vk.rb +34 -1
  93. data/lib/better_auth/social_providers/wechat.rb +6 -4
  94. data/lib/better_auth/sql_migration.rb +566 -0
  95. data/lib/better_auth/version.rb +1 -1
  96. data/lib/better_auth.rb +14 -41
  97. metadata +14 -18
  98. data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
  99. data/lib/better_auth/plugins/mcp/config.rb +0 -51
  100. data/lib/better_auth/plugins/mcp/consent.rb +0 -31
  101. data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
  102. data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
  103. data/lib/better_auth/plugins/mcp/registration.rb +0 -31
  104. data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
  105. data/lib/better_auth/plugins/mcp/schema.rb +0 -91
  106. data/lib/better_auth/plugins/mcp/token.rb +0 -108
  107. data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
  108. data/lib/better_auth/plugins/mcp.rb +0 -215
  109. data/lib/better_auth/plugins/oidc_provider.rb +0 -710
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "net/http"
5
+ require "timeout"
5
6
  require "uri"
6
7
 
7
8
  module BetterAuth
@@ -22,6 +23,7 @@ module BetterAuth
22
23
  "/sign-in/email",
23
24
  "/request-password-reset"
24
25
  ].freeze
26
+ CAPTCHA_VERIFY_TIMEOUT_SECONDS = 10
25
27
 
26
28
  CAPTCHA_SITE_VERIFY_URLS = {
27
29
  "cloudflare-turnstile" => "https://challenges.cloudflare.com/turnstile/v0/siteverify",
@@ -46,7 +48,11 @@ module BetterAuth
46
48
 
47
49
  def captcha_on_request(request, context, config)
48
50
  endpoints = Array(config[:endpoints]).empty? ? CAPTCHA_DEFAULT_ENDPOINTS : Array(config[:endpoints])
49
- return nil unless endpoints.any? { |endpoint| request.path_info.include?(endpoint.to_s) || request.url.include?(endpoint.to_s) }
51
+ path = request.path_info.to_s
52
+ email_otp_path = "/sign-in/email-otp"
53
+ email_otp_is_explicit = Array(config[:endpoints]).map(&:to_s).include?(email_otp_path)
54
+ return nil if path.include?(email_otp_path) && !email_otp_is_explicit
55
+ return nil unless endpoints.any? { |endpoint| path.include?(endpoint.to_s) }
50
56
 
51
57
  raise CAPTCHA_INTERNAL_ERROR_CODES["MISSING_SECRET_KEY"] if config[:secret_key].to_s.empty?
52
58
 
@@ -76,9 +82,13 @@ module BetterAuth
76
82
  min_score: config[:min_score],
77
83
  provider: provider
78
84
  }
79
- return captcha_normalize_verifier_response(config[:verifier].call(captcha_verifier_params(params))) if config[:verifier].respond_to?(:call)
80
-
81
- captcha_http_verify(params)
85
+ Timeout.timeout(CAPTCHA_VERIFY_TIMEOUT_SECONDS) do
86
+ if config[:verifier].respond_to?(:call)
87
+ captcha_normalize_verifier_response(config[:verifier].call(captcha_verifier_params(params)))
88
+ else
89
+ captcha_http_verify(params)
90
+ end
91
+ end
82
92
  end
83
93
 
84
94
  def captcha_verifier_params(params)
@@ -103,7 +113,7 @@ module BetterAuth
103
113
  else
104
114
  URI.encode_www_form(verifier[:payload])
105
115
  end
106
- response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
116
+ response = HTTPClient.request(uri, request, open_timeout: CAPTCHA_VERIFY_TIMEOUT_SECONDS, read_timeout: CAPTCHA_VERIFY_TIMEOUT_SECONDS)
107
117
  raise CAPTCHA_INTERNAL_ERROR_CODES["SERVICE_UNAVAILABLE"] unless response.is_a?(Net::HTTPSuccess)
108
118
 
109
119
  JSON.parse(response.body.to_s)
@@ -127,7 +137,15 @@ module BetterAuth
127
137
  return false unless result && result["success"]
128
138
 
129
139
  if config[:provider].to_s == "google-recaptcha" && result.key?("score")
130
- return result["score"].to_f >= (config[:min_score] || 0.5).to_f
140
+ return false if result["score"].to_f < (config[:min_score] || 0.5).to_f
141
+ end
142
+
143
+ if ["google-recaptcha", "cloudflare-turnstile"].include?(config[:provider].to_s)
144
+ expected_action = config[:expected_action].to_s
145
+ return false if !expected_action.empty? && result["action"] != expected_action
146
+
147
+ allowed_hostnames = Array(config[:allowed_hostnames]).map(&:to_s)
148
+ return false if allowed_hostnames.any? && !allowed_hostnames.include?(result["hostname"].to_s)
131
149
  end
132
150
 
133
151
  true
@@ -15,6 +15,7 @@ module BetterAuth
15
15
  "ACCESS_DENIED" => "Access denied",
16
16
  "INVALID_USER_CODE" => "Invalid user code",
17
17
  "DEVICE_CODE_ALREADY_PROCESSED" => "Device code already processed",
18
+ "DEVICE_CODE_NOT_CLAIMED" => "Device code has not been claimed by a verifying session; call `GET /device` with the `user_code` while signed in before approving or denying",
18
19
  "POLLING_TOO_FREQUENTLY" => "Polling too frequently",
19
20
  "USER_NOT_FOUND" => "User not found",
20
21
  "FAILED_TO_CREATE_SESSION" => "Failed to create session",
@@ -54,6 +55,15 @@ module BetterAuth
54
55
  openapi: {
55
56
  operationId: "requestDeviceCode",
56
57
  description: "Request a device and user code",
58
+ requestBody: OpenAPI.json_request_body(
59
+ OpenAPI.object_schema(
60
+ {
61
+ client_id: {type: "string", description: "OAuth client ID"},
62
+ user_id: {type: "string", description: "Optional user ID to pre-bind to the device code"},
63
+ scope: {type: "string", description: "Requested scopes"}
64
+ }
65
+ )
66
+ ),
57
67
  responses: {
58
68
  "200" => OpenAPI.json_response("Success", device_code_response_schema)
59
69
  }
@@ -77,6 +87,7 @@ module BetterAuth
77
87
  data: {
78
88
  "deviceCode" => device_code,
79
89
  "userCode" => user_code,
90
+ "userId" => body["user_id"].to_s.empty? ? nil : body["user_id"],
80
91
  "expiresAt" => Time.now + expires_in,
81
92
  "status" => "pending",
82
93
  "pollingInterval" => interval * 1000,
@@ -106,6 +117,16 @@ module BetterAuth
106
117
  openapi: {
107
118
  operationId: "exchangeDeviceToken",
108
119
  description: "Exchange device code for access token",
120
+ requestBody: OpenAPI.json_request_body(
121
+ OpenAPI.object_schema(
122
+ {
123
+ grant_type: {type: "string", enum: [OAuthProtocol::DEVICE_CODE_GRANT]},
124
+ device_code: {type: "string"},
125
+ client_id: {type: "string"}
126
+ },
127
+ required: ["grant_type", "device_code"]
128
+ )
129
+ ),
109
130
  responses: {
110
131
  "200" => OpenAPI.json_response("Success", device_token_response_schema)
111
132
  }
@@ -144,6 +165,18 @@ module BetterAuth
144
165
  ctx.context.adapter.delete(model: "deviceCode", where: [{field: "id", value: record["id"]}])
145
166
  raise device_authorization_error("BAD_REQUEST", "access_denied", DEVICE_AUTHORIZATION_ERROR_CODES["ACCESS_DENIED"])
146
167
  when "approved"
168
+ claimed = ctx.context.adapter.consume_one(
169
+ model: "deviceCode",
170
+ where: [
171
+ {field: "deviceCode", value: body["device_code"].to_s},
172
+ {field: "status", value: "approved"}
173
+ ]
174
+ )
175
+ unless claimed && claimed["userId"]
176
+ raise device_authorization_error("BAD_REQUEST", "invalid_grant", DEVICE_AUTHORIZATION_ERROR_CODES["INVALID_DEVICE_CODE"])
177
+ end
178
+
179
+ record = OAuthProtocol.stringify_keys(claimed)
147
180
  user = ctx.context.internal_adapter.find_user_by_id(record["userId"])
148
181
  raise device_authorization_error("INTERNAL_SERVER_ERROR", "server_error", DEVICE_AUTHORIZATION_ERROR_CODES["USER_NOT_FOUND"]) unless user
149
182
 
@@ -152,7 +185,6 @@ module BetterAuth
152
185
 
153
186
  session_data = {session: session, user: user}
154
187
  ctx.context.set_new_session(session_data) if ctx.context.respond_to?(:set_new_session)
155
- ctx.context.adapter.delete(model: "deviceCode", where: [{field: "id", value: record["id"]}])
156
188
  ctx.json({
157
189
  access_token: session["token"],
158
190
  token_type: "Bearer",
@@ -185,6 +217,21 @@ module BetterAuth
185
217
  record = OAuthProtocol.stringify_keys(record)
186
218
  raise device_authorization_error("BAD_REQUEST", "expired_token", DEVICE_AUTHORIZATION_ERROR_CODES["EXPIRED_USER_CODE"]) if device_authorization_time(record["expiresAt"]) <= Time.now
187
219
 
220
+ session = Routes.current_session(ctx, allow_nil: true)
221
+ if session && !record["userId"] && record["status"] == "pending"
222
+ claimed = ctx.context.adapter.increment_one(
223
+ model: "deviceCode",
224
+ where: [
225
+ {field: "id", value: record["id"]},
226
+ {field: "status", value: "pending"},
227
+ {field: "userId", value: nil}
228
+ ],
229
+ increment: {},
230
+ set: {"userId" => session[:user]["id"]}
231
+ )
232
+ record = OAuthProtocol.stringify_keys(claimed) if claimed
233
+ end
234
+
188
235
  ctx.json({user_code: code, status: record["status"]})
189
236
  end
190
237
  end
@@ -197,6 +244,14 @@ module BetterAuth
197
244
  openapi: {
198
245
  operationId: "approveDevice",
199
246
  description: "Approve a device authorization request",
247
+ requestBody: OpenAPI.json_request_body(
248
+ OpenAPI.object_schema(
249
+ {
250
+ userCode: {type: "string", description: "User code shown on the device"}
251
+ },
252
+ required: ["userCode"]
253
+ )
254
+ ),
200
255
  responses: {
201
256
  "200" => OpenAPI.json_response("Success", OpenAPI.success_response_schema)
202
257
  }
@@ -218,6 +273,14 @@ module BetterAuth
218
273
  openapi: {
219
274
  operationId: "denyDevice",
220
275
  description: "Deny a device authorization request",
276
+ requestBody: OpenAPI.json_request_body(
277
+ OpenAPI.object_schema(
278
+ {
279
+ userCode: {type: "string", description: "User code shown on the device"}
280
+ },
281
+ required: ["userCode"]
282
+ )
283
+ ),
221
284
  responses: {
222
285
  "200" => OpenAPI.json_response("Success", OpenAPI.success_response_schema)
223
286
  }
@@ -240,15 +303,24 @@ module BetterAuth
240
303
  record = OAuthProtocol.stringify_keys(record)
241
304
  raise device_authorization_error("BAD_REQUEST", "expired_token", DEVICE_AUTHORIZATION_ERROR_CODES["EXPIRED_USER_CODE"]) if device_authorization_time(record["expiresAt"]) <= Time.now
242
305
  raise device_authorization_error("BAD_REQUEST", "invalid_request", DEVICE_AUTHORIZATION_ERROR_CODES["DEVICE_CODE_ALREADY_PROCESSED"]) unless record["status"] == "pending"
243
- if record["userId"] && record["userId"] != session[:user]["id"]
306
+ unless record["userId"]
307
+ raise device_authorization_error("BAD_REQUEST", "invalid_request", DEVICE_AUTHORIZATION_ERROR_CODES["DEVICE_CODE_NOT_CLAIMED"])
308
+ end
309
+ if record["userId"] != session[:user]["id"]
244
310
  raise device_authorization_error("FORBIDDEN", "access_denied", "You are not authorized to #{action} this device authorization")
245
311
  end
246
312
 
247
- ctx.context.adapter.update(
313
+ updated = ctx.context.adapter.increment_one(
248
314
  model: "deviceCode",
249
- where: [{field: "id", value: record["id"]}],
250
- update: {"status" => status, "userId" => record["userId"] || session[:user]["id"]}
315
+ where: [
316
+ {field: "id", value: record["id"]},
317
+ {field: "status", value: "pending"},
318
+ {field: "userId", value: session[:user]["id"]}
319
+ ],
320
+ increment: {},
321
+ set: {"status" => status, "userId" => session[:user]["id"]}
251
322
  )
323
+ raise device_authorization_error("BAD_REQUEST", "invalid_request", DEVICE_AUTHORIZATION_ERROR_CODES["DEVICE_CODE_ALREADY_PROCESSED"]) unless updated
252
324
  ctx.json({success: true})
253
325
  end
254
326
 
@@ -8,7 +8,7 @@ module BetterAuth
8
8
  config = normalize_hash(options)
9
9
  oauth_plugin = dub_oauth_plugin(config[:oauth])
10
10
  endpoints = {dub_link: dub_link_endpoint(oauth_plugin)}
11
- endpoints[:dub_o_auth2_callback] = oauth_plugin.endpoints.fetch(:o_auth2_callback) if oauth_plugin
11
+ endpoints[:dub_oauth2_callback] = oauth_plugin.endpoints.fetch(:oauth2_callback) if oauth_plugin
12
12
 
13
13
  Plugin.new(
14
14
  id: "dub",
@@ -38,6 +38,14 @@ module BetterAuth
38
38
  openapi: {
39
39
  operationId: "dubLink",
40
40
  description: "Link a Dub OAuth account",
41
+ requestBody: OpenAPI.json_request_body(
42
+ OpenAPI.object_schema(
43
+ {
44
+ callbackURL: {type: "string", description: "The URL to redirect to after linking"},
45
+ callback_url: {type: "string", description: "The URL to redirect to after linking"}
46
+ }
47
+ )
48
+ ),
41
49
  responses: {
42
50
  "200" => OpenAPI.json_response(
43
51
  "Authorization URL generated successfully for linking a Dub account",
@@ -65,7 +73,7 @@ module BetterAuth
65
73
  Routes.validate_auth_callback_url!(ctx.context, callback_url, "callbackURL")
66
74
 
67
75
  ctx.body = body.merge(provider_id: "dub", callback_url: callback_url)
68
- oauth_plugin.endpoints.fetch(:o_auth2_link_account).call(ctx)
76
+ oauth_plugin.endpoints.fetch(:oauth2_link_account).call(ctx)
69
77
  end
70
78
  end
71
79
 
@@ -29,7 +29,6 @@ module BetterAuth
29
29
  verify_email_otp: verify_email_otp_endpoint(config),
30
30
  sign_in_email_otp: sign_in_email_otp_endpoint(config),
31
31
  request_password_reset_email_otp: request_password_reset_email_otp_endpoint(config),
32
- forget_password_email_otp: forget_password_email_otp_endpoint(config),
33
32
  reset_password_email_otp: reset_password_email_otp_endpoint(config),
34
33
  request_email_change_email_otp: request_email_change_email_otp_endpoint(config),
35
34
  change_email_email_otp: change_email_email_otp_endpoint(config)
@@ -119,7 +118,7 @@ module BetterAuth
119
118
  end
120
119
 
121
120
  def create_verification_otp_endpoint(config)
122
- Endpoint.new(method: "POST") do |ctx|
121
+ Endpoint.new(method: "POST", metadata: {server_only: true}) do |ctx|
123
122
  body = normalize_hash(ctx.body)
124
123
  email = body[:email].to_s.downcase
125
124
  type = body[:type].to_s
@@ -133,9 +132,9 @@ module BetterAuth
133
132
 
134
133
  def get_verification_otp_endpoint(config)
135
134
  Endpoint.new(
136
- path: "/email-otp/get-verification-otp",
137
135
  method: "GET",
138
136
  metadata: {
137
+ server_only: true,
139
138
  openapi: {
140
139
  operationId: "getVerificationOTP",
141
140
  description: "Get a stored verification OTP when storage allows plaintext access",
@@ -326,7 +325,13 @@ module BetterAuth
326
325
  end
327
326
 
328
327
  unless user["emailVerified"]
329
- user = ctx.context.internal_adapter.update_user(user["id"], emailVerified: true)
328
+ user = ctx.context.adapter.transaction do
329
+ ctx.context.internal_adapter.revoke_unproven_account_access(user["id"])
330
+ updated = ctx.context.internal_adapter.update_user(user["id"], emailVerified: true)
331
+ raise Error, "Failed to verify email-OTP user" unless updated
332
+
333
+ updated
334
+ end
330
335
  end
331
336
 
332
337
  session = ctx.context.internal_adapter.create_session(user["id"])
@@ -453,32 +458,6 @@ module BetterAuth
453
458
  end
454
459
  end
455
460
 
456
- def forget_password_email_otp_endpoint(config)
457
- Endpoint.new(
458
- path: "/forget-password/email-otp",
459
- method: "POST",
460
- metadata: {
461
- openapi: {
462
- operationId: "forgetPasswordEmailOTP",
463
- description: "Request a password reset OTP by email",
464
- requestBody: OpenAPI.json_request_body(
465
- OpenAPI.object_schema(
466
- {
467
- email: {type: "string"}
468
- },
469
- required: ["email"]
470
- )
471
- ),
472
- responses: {
473
- "200" => OpenAPI.json_response("Password reset OTP requested", OpenAPI.status_response_schema)
474
- }
475
- }
476
- }
477
- ) do |ctx|
478
- email_otp_password_reset_request(ctx, config)
479
- end
480
- end
481
-
482
461
  def reset_password_email_otp_endpoint(config)
483
462
  Endpoint.new(
484
463
  path: "/email-otp/reset-password",
@@ -524,7 +503,7 @@ module BetterAuth
524
503
  ctx.context.internal_adapter.update_user(found[:user]["id"], emailVerified: true) unless found[:user]["emailVerified"]
525
504
  callback = ctx.context.options.email_and_password[:on_password_reset]
526
505
  callback.call({user: found[:user]}, ctx.request) if callback.respond_to?(:call)
527
- ctx.context.internal_adapter.delete_sessions(found[:user]["id"]) if ctx.context.options.email_and_password[:revoke_sessions_on_password_reset]
506
+ ctx.context.internal_adapter.delete_user_sessions(found[:user]["id"]) if ctx.context.options.email_and_password[:revoke_sessions_on_password_reset]
528
507
  ctx.json({success: true})
529
508
  end
530
509
  end
@@ -610,35 +589,40 @@ module BetterAuth
610
589
  end
611
590
 
612
591
  def email_otp_verify!(ctx, config, email:, type:, otp:, consume: true)
613
- verification = ctx.context.internal_adapter.find_verification_value(email_otp_identifier(email, type))
614
- raise APIError.new("BAD_REQUEST", message: EMAIL_OTP_ERROR_CODES["INVALID_OTP"]) unless verification
592
+ identifier = email_otp_identifier(email, type)
593
+ existing = ctx.context.internal_adapter.find_verification_value(identifier)
615
594
 
616
- if Routes.expired_time?(verification["expiresAt"])
617
- ctx.context.internal_adapter.delete_verification_value(verification["id"])
595
+ if existing && Routes.expired_time?(existing["expiresAt"])
596
+ ctx.context.internal_adapter.delete_verification_by_identifier(identifier)
618
597
  raise APIError.new("BAD_REQUEST", message: EMAIL_OTP_ERROR_CODES["OTP_EXPIRED"])
619
598
  end
620
599
 
600
+ verification = ctx.context.internal_adapter.consume_verification_value(identifier)
601
+ raise APIError.new("BAD_REQUEST", message: EMAIL_OTP_ERROR_CODES["INVALID_OTP"]) unless verification
602
+
621
603
  otp_value, attempts = email_otp_split(verification["value"])
622
604
  attempts_count = attempts.to_i
623
605
  if attempts_count >= config[:allowed_attempts].to_i
624
- ctx.context.internal_adapter.delete_verification_value(verification["id"])
625
606
  raise APIError.new("FORBIDDEN", message: EMAIL_OTP_ERROR_CODES["TOO_MANY_ATTEMPTS"])
626
607
  end
627
608
 
628
- ctx.context.internal_adapter.delete_verification_value(verification["id"]) if consume
629
609
  unless email_otp_matches?(ctx, config, otp_value, otp)
630
- if consume
631
- ctx.context.internal_adapter.create_verification_value(
632
- identifier: email_otp_identifier(email, type),
633
- value: "#{otp_value}:#{attempts_count + 1}",
634
- expiresAt: verification["expiresAt"]
635
- )
636
- else
637
- ctx.context.internal_adapter.update_verification_value(verification["id"], value: "#{otp_value}:#{attempts_count + 1}")
638
- end
610
+ ctx.context.internal_adapter.create_verification_value(
611
+ identifier: identifier,
612
+ value: "#{otp_value}:#{attempts_count + 1}",
613
+ expiresAt: verification["expiresAt"]
614
+ )
639
615
  raise APIError.new("BAD_REQUEST", message: EMAIL_OTP_ERROR_CODES["INVALID_OTP"])
640
616
  end
641
617
 
618
+ unless consume
619
+ ctx.context.internal_adapter.create_verification_value(
620
+ identifier: identifier,
621
+ value: verification["value"],
622
+ expiresAt: verification["expiresAt"]
623
+ )
624
+ end
625
+
642
626
  true
643
627
  end
644
628
 
@@ -763,7 +747,6 @@ module BetterAuth
763
747
  /sign-in/email-otp
764
748
  /email-otp/request-password-reset
765
749
  /email-otp/reset-password
766
- /forget-password/email-otp
767
750
  /email-otp/request-email-change
768
751
  /email-otp/change-email
769
752
  ].map do |path|