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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +92 -0
- data/LICENSE.md +1 -1
- data/README.md +142 -95
- data/lib/better_auth/adapters/base.rb +64 -1
- data/lib/better_auth/adapters/internal_adapter.rb +197 -11
- data/lib/better_auth/adapters/join_support.rb +10 -0
- data/lib/better_auth/adapters/memory.rb +303 -54
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +390 -63
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +48 -47
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +119 -15
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +108 -0
- data/lib/better_auth/endpoint.rb +88 -5
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/http_client.rb +46 -0
- data/lib/better_auth/logger.rb +1 -1
- data/lib/better_auth/middleware/origin_check.rb +9 -1
- data/lib/better_auth/migration_plan.rb +15 -0
- data/lib/better_auth/oauth2.rb +1 -1
- data/lib/better_auth/oauth_state.rb +147 -0
- data/lib/better_auth/plugin_loader.rb +220 -0
- data/lib/better_auth/plugins/access.rb +10 -13
- data/lib/better_auth/plugins/admin.rb +85 -29
- data/lib/better_auth/plugins/anonymous.rb +2 -0
- data/lib/better_auth/plugins/api_key.rb +4 -7
- data/lib/better_auth/plugins/bearer.rb +8 -22
- data/lib/better_auth/plugins/captcha.rb +24 -6
- data/lib/better_auth/plugins/device_authorization.rb +77 -5
- data/lib/better_auth/plugins/dub.rb +10 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +162 -140
- data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +44 -19
- data/lib/better_auth/plugins/magic_link.rb +18 -21
- data/lib/better_auth/plugins/oauth_popup.rb +330 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +233 -46
- data/lib/better_auth/plugins/oauth_provider.rb +4 -7
- data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
- data/lib/better_auth/plugins/one_tap.rb +78 -65
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +77 -9
- data/lib/better_auth/plugins/organization.rb +546 -102
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +25 -27
- data/lib/better_auth/plugins/scim.rb +4 -7
- data/lib/better_auth/plugins/siwe.rb +126 -16
- data/lib/better_auth/plugins/sso.rb +4 -7
- data/lib/better_auth/plugins/stripe.rb +4 -7
- data/lib/better_auth/plugins/two_factor.rb +205 -19
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +300 -133
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +44 -8
- data/lib/better_auth/routes/email_verification.rb +93 -6
- data/lib/better_auth/routes/password.rb +6 -6
- data/lib/better_auth/routes/session.rb +7 -2
- data/lib/better_auth/routes/sign_in.rb +10 -1
- data/lib/better_auth/routes/sign_up.rb +45 -22
- data/lib/better_auth/routes/social.rb +197 -54
- data/lib/better_auth/routes/user.rb +17 -10
- data/lib/better_auth/schema/sql.rb +189 -34
- data/lib/better_auth/schema.rb +159 -5
- data/lib/better_auth/session.rb +29 -10
- data/lib/better_auth/session_store.rb +1 -1
- data/lib/better_auth/social_providers/apple.rb +6 -1
- data/lib/better_auth/social_providers/atlassian.rb +1 -0
- data/lib/better_auth/social_providers/base.rb +45 -23
- data/lib/better_auth/social_providers/cognito.rb +75 -5
- data/lib/better_auth/social_providers/facebook.rb +95 -1
- data/lib/better_auth/social_providers/figma.rb +2 -0
- data/lib/better_auth/social_providers/github.rb +2 -0
- data/lib/better_auth/social_providers/google.rb +12 -1
- data/lib/better_auth/social_providers/line.rb +35 -1
- data/lib/better_auth/social_providers/linear.rb +1 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
- data/lib/better_auth/social_providers/naver.rb +24 -1
- data/lib/better_auth/social_providers/notion.rb +1 -0
- data/lib/better_auth/social_providers/paybin.rb +25 -1
- data/lib/better_auth/social_providers/paypal.rb +118 -2
- data/lib/better_auth/social_providers/reddit.rb +25 -4
- data/lib/better_auth/social_providers/salesforce.rb +1 -0
- data/lib/better_auth/social_providers/twitch.rb +24 -1
- data/lib/better_auth/social_providers/twitter.rb +36 -1
- data/lib/better_auth/social_providers/vk.rb +34 -1
- data/lib/better_auth/social_providers/wechat.rb +6 -4
- data/lib/better_auth/sql_migration.rb +566 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +14 -41
- metadata +14 -18
- data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
- data/lib/better_auth/plugins/mcp/config.rb +0 -51
- data/lib/better_auth/plugins/mcp/consent.rb +0 -31
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
- data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
- data/lib/better_auth/plugins/mcp/registration.rb +0 -31
- data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
- data/lib/better_auth/plugins/mcp/schema.rb +0 -91
- data/lib/better_auth/plugins/mcp/token.rb +0 -108
- data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
- data/lib/better_auth/plugins/mcp.rb +0 -215
- 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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
313
|
+
updated = ctx.context.adapter.increment_one(
|
|
248
314
|
model: "deviceCode",
|
|
249
|
-
where: [
|
|
250
|
-
|
|
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[:
|
|
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(:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
614
|
-
|
|
592
|
+
identifier = email_otp_identifier(email, type)
|
|
593
|
+
existing = ctx.context.internal_adapter.find_verification_value(identifier)
|
|
615
594
|
|
|
616
|
-
if Routes.expired_time?(
|
|
617
|
-
ctx.context.internal_adapter.
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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|
|