better_auth 0.10.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 +83 -0
- data/LICENSE.md +1 -1
- data/README.md +138 -91
- 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 +191 -56
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +294 -49
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +41 -46
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +110 -14
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +14 -3
- data/lib/better_auth/error.rb +3 -1
- 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 +4 -4
- 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 +79 -28
- 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 +47 -9
- data/lib/better_auth/plugins/dub.rb +2 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +141 -146
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +37 -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 +142 -32
- 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 +61 -60
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +35 -7
- data/lib/better_auth/plugins/organization.rb +455 -122
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +24 -26
- 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 +186 -21
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +297 -135
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +40 -8
- data/lib/better_auth/routes/email_verification.rb +88 -5
- data/lib/better_auth/routes/password.rb +5 -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 +169 -54
- data/lib/better_auth/routes/user.rb +11 -8
- data/lib/better_auth/schema/sql.rb +90 -24
- data/lib/better_auth/schema.rb +124 -3
- data/lib/better_auth/session.rb +27 -9
- 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 +41 -14
- 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/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 +5 -5
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +11 -41
- metadata +5 -13
- 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 -256
- data/lib/better_auth/plugins/oidc_provider.rb +0 -769
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
return if defined?(BetterAuth::Plugins::API_KEY_PLUGIN_IMPLEMENTATION)
|
|
4
|
+
|
|
3
5
|
module BetterAuth
|
|
4
6
|
module Plugins
|
|
5
7
|
module_function
|
|
6
8
|
|
|
7
|
-
def api_key(*args)
|
|
8
|
-
|
|
9
|
-
BetterAuth::Plugins.api_key(*args)
|
|
10
|
-
rescue LoadError => error
|
|
11
|
-
raise if error.path && error.path != "better_auth/api_key"
|
|
12
|
-
|
|
13
|
-
raise LoadError, "BetterAuth::Plugins.api_key requires the better_auth-api-key gem. Add `gem \"better_auth-api-key\"` and `require \"better_auth/api_key\"`."
|
|
9
|
+
def api_key(*args, &block)
|
|
10
|
+
call_external_plugin!(:api_key, *args, implementation_constant: :API_KEY_PLUGIN_IMPLEMENTATION, gem_name: "better_auth-api-key", entry: "lib/better_auth/api_key.rb", &block)
|
|
14
11
|
end
|
|
15
12
|
end
|
|
16
13
|
end
|
|
@@ -50,7 +50,7 @@ module BetterAuth
|
|
|
50
50
|
return unless signed_token && valid_signed_token?(ctx, signed_token)
|
|
51
51
|
|
|
52
52
|
cookie_name = ctx.context.auth_cookies[:session_token].name
|
|
53
|
-
cookie =
|
|
53
|
+
cookie = Cookies.set_request_cookie(ctx.headers["cookie"], cookie_name, signed_token)
|
|
54
54
|
{context: {headers: ctx.headers.merge("cookie" => cookie)}}
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -86,37 +86,23 @@ module BetterAuth
|
|
|
86
86
|
token.to_s
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
def bearer_session_cookie(line)
|
|
90
|
-
first, *attributes = line.to_s.split(";").map(&:strip)
|
|
91
|
-
name, value = first.split("=", 2)
|
|
92
|
-
return unless name && value
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
name: name,
|
|
96
|
-
value: value,
|
|
97
|
-
attributes: attributes.each_with_object({}) do |attribute, result|
|
|
98
|
-
key, attribute_value = attribute.split("=", 2)
|
|
99
|
-
result[key.to_s.downcase] = attribute_value || true unless key.to_s.empty?
|
|
100
|
-
end
|
|
101
|
-
}
|
|
102
|
-
end
|
|
103
|
-
|
|
104
89
|
def expired_bearer_cookie?(cookie)
|
|
105
90
|
max_age = cookie[:attributes]["max-age"]
|
|
106
91
|
max_age.to_s.strip.match?(/\A[+-]?\d+\z/) && max_age.to_i == 0
|
|
107
92
|
end
|
|
108
93
|
|
|
109
94
|
def expose_auth_token(ctx)
|
|
110
|
-
set_cookie = ctx.response_headers["set-cookie"].to_s
|
|
111
95
|
token_name = ctx.context.auth_cookies[:session_token].name
|
|
112
|
-
|
|
113
|
-
cookie =
|
|
96
|
+
session_cookie = Cookies.split_set_cookie_header(ctx.response_headers["set-cookie"]).reverse_each.filter_map do |line|
|
|
97
|
+
cookie = Cookies.parse_set_cookie(line)
|
|
114
98
|
next unless cookie && cookie[:name] == token_name
|
|
115
|
-
next if cookie[:value].empty? || expired_bearer_cookie?(cookie)
|
|
116
99
|
|
|
117
|
-
cookie
|
|
100
|
+
cookie
|
|
118
101
|
end.first
|
|
119
|
-
return unless
|
|
102
|
+
return unless session_cookie
|
|
103
|
+
return if session_cookie[:value].empty? || expired_bearer_cookie?(session_cookie)
|
|
104
|
+
|
|
105
|
+
token = session_cookie[:value]
|
|
120
106
|
|
|
121
107
|
exposed = ctx.response_headers["access-control-expose-headers"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
122
108
|
exposed << "set-auth-token"
|
|
@@ -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 = HTTPClient.request(uri, 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
|
|
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",
|
|
@@ -58,6 +59,7 @@ module BetterAuth
|
|
|
58
59
|
OpenAPI.object_schema(
|
|
59
60
|
{
|
|
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"},
|
|
61
63
|
scope: {type: "string", description: "Requested scopes"}
|
|
62
64
|
}
|
|
63
65
|
)
|
|
@@ -85,6 +87,7 @@ module BetterAuth
|
|
|
85
87
|
data: {
|
|
86
88
|
"deviceCode" => device_code,
|
|
87
89
|
"userCode" => user_code,
|
|
90
|
+
"userId" => body["user_id"].to_s.empty? ? nil : body["user_id"],
|
|
88
91
|
"expiresAt" => Time.now + expires_in,
|
|
89
92
|
"status" => "pending",
|
|
90
93
|
"pollingInterval" => interval * 1000,
|
|
@@ -162,6 +165,18 @@ module BetterAuth
|
|
|
162
165
|
ctx.context.adapter.delete(model: "deviceCode", where: [{field: "id", value: record["id"]}])
|
|
163
166
|
raise device_authorization_error("BAD_REQUEST", "access_denied", DEVICE_AUTHORIZATION_ERROR_CODES["ACCESS_DENIED"])
|
|
164
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)
|
|
165
180
|
user = ctx.context.internal_adapter.find_user_by_id(record["userId"])
|
|
166
181
|
raise device_authorization_error("INTERNAL_SERVER_ERROR", "server_error", DEVICE_AUTHORIZATION_ERROR_CODES["USER_NOT_FOUND"]) unless user
|
|
167
182
|
|
|
@@ -170,7 +185,6 @@ module BetterAuth
|
|
|
170
185
|
|
|
171
186
|
session_data = {session: session, user: user}
|
|
172
187
|
ctx.context.set_new_session(session_data) if ctx.context.respond_to?(:set_new_session)
|
|
173
|
-
ctx.context.adapter.delete(model: "deviceCode", where: [{field: "id", value: record["id"]}])
|
|
174
188
|
ctx.json({
|
|
175
189
|
access_token: session["token"],
|
|
176
190
|
token_type: "Bearer",
|
|
@@ -203,6 +217,21 @@ module BetterAuth
|
|
|
203
217
|
record = OAuthProtocol.stringify_keys(record)
|
|
204
218
|
raise device_authorization_error("BAD_REQUEST", "expired_token", DEVICE_AUTHORIZATION_ERROR_CODES["EXPIRED_USER_CODE"]) if device_authorization_time(record["expiresAt"]) <= Time.now
|
|
205
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
|
+
|
|
206
235
|
ctx.json({user_code: code, status: record["status"]})
|
|
207
236
|
end
|
|
208
237
|
end
|
|
@@ -218,9 +247,9 @@ module BetterAuth
|
|
|
218
247
|
requestBody: OpenAPI.json_request_body(
|
|
219
248
|
OpenAPI.object_schema(
|
|
220
249
|
{
|
|
221
|
-
user_code: {type: "string", description: "User code shown on the device"},
|
|
222
250
|
userCode: {type: "string", description: "User code shown on the device"}
|
|
223
|
-
}
|
|
251
|
+
},
|
|
252
|
+
required: ["userCode"]
|
|
224
253
|
)
|
|
225
254
|
),
|
|
226
255
|
responses: {
|
|
@@ -247,9 +276,9 @@ module BetterAuth
|
|
|
247
276
|
requestBody: OpenAPI.json_request_body(
|
|
248
277
|
OpenAPI.object_schema(
|
|
249
278
|
{
|
|
250
|
-
user_code: {type: "string", description: "User code shown on the device"},
|
|
251
279
|
userCode: {type: "string", description: "User code shown on the device"}
|
|
252
|
-
}
|
|
280
|
+
},
|
|
281
|
+
required: ["userCode"]
|
|
253
282
|
)
|
|
254
283
|
),
|
|
255
284
|
responses: {
|
|
@@ -274,15 +303,24 @@ module BetterAuth
|
|
|
274
303
|
record = OAuthProtocol.stringify_keys(record)
|
|
275
304
|
raise device_authorization_error("BAD_REQUEST", "expired_token", DEVICE_AUTHORIZATION_ERROR_CODES["EXPIRED_USER_CODE"]) if device_authorization_time(record["expiresAt"]) <= Time.now
|
|
276
305
|
raise device_authorization_error("BAD_REQUEST", "invalid_request", DEVICE_AUTHORIZATION_ERROR_CODES["DEVICE_CODE_ALREADY_PROCESSED"]) unless record["status"] == "pending"
|
|
277
|
-
|
|
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"]
|
|
278
310
|
raise device_authorization_error("FORBIDDEN", "access_denied", "You are not authorized to #{action} this device authorization")
|
|
279
311
|
end
|
|
280
312
|
|
|
281
|
-
ctx.context.adapter.
|
|
313
|
+
updated = ctx.context.adapter.increment_one(
|
|
282
314
|
model: "deviceCode",
|
|
283
|
-
where: [
|
|
284
|
-
|
|
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"]}
|
|
285
322
|
)
|
|
323
|
+
raise device_authorization_error("BAD_REQUEST", "invalid_request", DEVICE_AUTHORIZATION_ERROR_CODES["DEVICE_CODE_ALREADY_PROCESSED"]) unless updated
|
|
286
324
|
ctx.json({success: true})
|
|
287
325
|
end
|
|
288
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",
|
|
@@ -73,7 +73,7 @@ module BetterAuth
|
|
|
73
73
|
Routes.validate_auth_callback_url!(ctx.context, callback_url, "callbackURL")
|
|
74
74
|
|
|
75
75
|
ctx.body = body.merge(provider_id: "dub", callback_url: callback_url)
|
|
76
|
-
oauth_plugin.endpoints.fetch(:
|
|
76
|
+
oauth_plugin.endpoints.fetch(:oauth2_link_account).call(ctx)
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
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|
|