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::PASSKEY_PLUGIN_IMPLEMENTATION)
|
|
4
|
+
|
|
3
5
|
module BetterAuth
|
|
4
6
|
module Plugins
|
|
5
7
|
module_function
|
|
6
8
|
|
|
7
|
-
def passkey(*args)
|
|
8
|
-
|
|
9
|
-
BetterAuth::Plugins.passkey(*args)
|
|
10
|
-
rescue LoadError => error
|
|
11
|
-
raise if error.path && error.path != "better_auth/passkey"
|
|
12
|
-
|
|
13
|
-
raise LoadError, "BetterAuth::Plugins.passkey requires the better_auth-passkey gem. Add `gem \"better_auth-passkey\"` and `require \"better_auth/passkey\"`."
|
|
9
|
+
def passkey(*args, &block)
|
|
10
|
+
call_external_plugin!(:passkey, *args, implementation_constant: :PASSKEY_PLUGIN_IMPLEMENTATION, gem_name: "better_auth-passkey", entry: "lib/better_auth/passkey.rb", &block)
|
|
14
11
|
end
|
|
15
12
|
end
|
|
16
13
|
end
|
|
@@ -110,7 +110,7 @@ module BetterAuth
|
|
|
110
110
|
|
|
111
111
|
if config[:require_verification] && !found["phoneNumberVerified"]
|
|
112
112
|
code = phone_number_generate_code(config)
|
|
113
|
-
phone_number_store_code(ctx, config, phone_number, code)
|
|
113
|
+
phone_number_store_code(ctx, config, phone_number, code) unless config[:verify_otp].respond_to?(:call)
|
|
114
114
|
phone_number_deliver_otp(config, {phone_number: phone_number, code: code}, ctx)
|
|
115
115
|
raise APIError.new("UNAUTHORIZED", message: PHONE_NUMBER_ERROR_CODES["PHONE_NUMBER_NOT_VERIFIED"])
|
|
116
116
|
end
|
|
@@ -170,7 +170,7 @@ module BetterAuth
|
|
|
170
170
|
phone_number = body[:phone_number].to_s
|
|
171
171
|
validate_phone_number!(config, phone_number)
|
|
172
172
|
code = phone_number_generate_code(config)
|
|
173
|
-
phone_number_store_code(ctx, config, phone_number, code)
|
|
173
|
+
phone_number_store_code(ctx, config, phone_number, code) unless config[:verify_otp].respond_to?(:call)
|
|
174
174
|
phone_number_deliver_otp(config, {phone_number: phone_number, code: code}, ctx)
|
|
175
175
|
ctx.json({message: "code sent"})
|
|
176
176
|
end
|
|
@@ -280,7 +280,7 @@ module BetterAuth
|
|
|
280
280
|
phone_number = body[:phone_number].to_s
|
|
281
281
|
user = ctx.context.adapter.find_one(model: "user", where: [{field: "phoneNumber", value: phone_number}])
|
|
282
282
|
code = phone_number_generate_code(config)
|
|
283
|
-
phone_number_store_code(ctx, config, "#{phone_number}-request-password-reset", code)
|
|
283
|
+
phone_number_store_code(ctx, config, "#{phone_number}-request-password-reset", code) unless config[:verify_otp].respond_to?(:call)
|
|
284
284
|
|
|
285
285
|
if user && config[:send_password_reset_otp].respond_to?(:call)
|
|
286
286
|
config[:send_password_reset_otp].call({phone_number: phone_number, code: code}, ctx)
|
|
@@ -319,14 +319,13 @@ module BetterAuth
|
|
|
319
319
|
otp = body[:otp].to_s
|
|
320
320
|
new_password = body[:new_password]
|
|
321
321
|
|
|
322
|
-
verification = phone_number_verify_code!(ctx, config, "#{phone_number}-request-password-reset", otp, consume: false)
|
|
323
322
|
user = ctx.context.adapter.find_one(model: "user", where: [{field: "phoneNumber", value: phone_number}])
|
|
324
323
|
raise APIError.new("BAD_REQUEST", message: PHONE_NUMBER_ERROR_CODES["UNEXPECTED_ERROR"]) unless user
|
|
325
324
|
|
|
326
325
|
Routes.validate_password_length!(new_password, ctx.context.options.email_and_password)
|
|
326
|
+
phone_number_verify_code!(ctx, config, "#{phone_number}-request-password-reset", otp)
|
|
327
327
|
ctx.context.internal_adapter.update_password(user["id"], Routes.hash_password(ctx, new_password))
|
|
328
|
-
ctx.context.internal_adapter.
|
|
329
|
-
ctx.context.internal_adapter.delete_sessions(user["id"]) if ctx.context.options.email_and_password[:revoke_sessions_on_password_reset]
|
|
328
|
+
ctx.context.internal_adapter.delete_user_sessions(user["id"]) if ctx.context.options.email_and_password[:revoke_sessions_on_password_reset]
|
|
330
329
|
ctx.json({status: true})
|
|
331
330
|
end
|
|
332
331
|
end
|
|
@@ -367,43 +366,52 @@ module BetterAuth
|
|
|
367
366
|
def phone_number_verify_code!(ctx, config, identifier, code, consume: true)
|
|
368
367
|
verifier = config[:verify_otp]
|
|
369
368
|
if verifier.respond_to?(:call)
|
|
369
|
+
# Custom verify_otp owns single-use and expiry state completely.
|
|
370
370
|
valid = verifier.call({phone_number: identifier.delete_suffix("-request-password-reset"), code: code}, ctx)
|
|
371
371
|
raise APIError.new("BAD_REQUEST", message: PHONE_NUMBER_ERROR_CODES["INVALID_OTP"]) unless valid
|
|
372
372
|
|
|
373
|
-
|
|
374
|
-
ctx.context.internal_adapter.delete_verification_value(verification["id"]) if consume && verification
|
|
375
|
-
return verification || true
|
|
373
|
+
return true
|
|
376
374
|
end
|
|
377
375
|
|
|
378
|
-
|
|
379
|
-
raise APIError.new("BAD_REQUEST", message: PHONE_NUMBER_ERROR_CODES["OTP_NOT_FOUND"]) unless
|
|
376
|
+
existing = ctx.context.internal_adapter.find_verification_value(identifier)
|
|
377
|
+
raise APIError.new("BAD_REQUEST", message: PHONE_NUMBER_ERROR_CODES["OTP_NOT_FOUND"]) unless existing
|
|
380
378
|
|
|
381
|
-
if Routes.expired_time?(
|
|
379
|
+
if Routes.expired_time?(existing["expiresAt"])
|
|
380
|
+
ctx.context.internal_adapter.delete_verification_by_identifier(identifier)
|
|
382
381
|
raise APIError.new("BAD_REQUEST", message: PHONE_NUMBER_ERROR_CODES["OTP_EXPIRED"])
|
|
383
382
|
end
|
|
384
383
|
|
|
384
|
+
_, existing_attempts = phone_number_split_code(existing["value"])
|
|
385
|
+
if existing_attempts.to_i >= config[:allowed_attempts].to_i
|
|
386
|
+
ctx.context.internal_adapter.delete_verification_by_identifier(identifier)
|
|
387
|
+
raise APIError.new("FORBIDDEN", message: PHONE_NUMBER_ERROR_CODES["TOO_MANY_ATTEMPTS"])
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
verification = ctx.context.internal_adapter.consume_verification_value(identifier)
|
|
391
|
+
raise APIError.new("BAD_REQUEST", message: PHONE_NUMBER_ERROR_CODES["INVALID_OTP"]) unless verification
|
|
392
|
+
|
|
385
393
|
stored_code, attempts = phone_number_split_code(verification["value"])
|
|
386
394
|
attempts_count = attempts.to_i
|
|
387
395
|
if attempts_count >= config[:allowed_attempts].to_i
|
|
388
|
-
ctx.context.internal_adapter.delete_verification_value(verification["id"])
|
|
389
396
|
raise APIError.new("FORBIDDEN", message: PHONE_NUMBER_ERROR_CODES["TOO_MANY_ATTEMPTS"])
|
|
390
397
|
end
|
|
391
398
|
|
|
392
399
|
unless stored_code == code
|
|
393
|
-
ctx.context.internal_adapter.
|
|
400
|
+
ctx.context.internal_adapter.create_verification_value(identifier: identifier, value: "#{stored_code}:#{attempts_count + 1}", expiresAt: verification["expiresAt"])
|
|
394
401
|
raise APIError.new("BAD_REQUEST", message: PHONE_NUMBER_ERROR_CODES["INVALID_OTP"])
|
|
395
402
|
end
|
|
396
403
|
|
|
397
|
-
ctx.context.internal_adapter.
|
|
404
|
+
ctx.context.internal_adapter.create_verification_value(identifier: identifier, value: verification["value"], expiresAt: verification["expiresAt"]) unless consume
|
|
398
405
|
verification
|
|
399
406
|
end
|
|
400
407
|
|
|
401
408
|
def phone_number_store_code(ctx, config, identifier, code)
|
|
402
409
|
ctx.context.internal_adapter.delete_verification_by_identifier(identifier)
|
|
410
|
+
expires_at = Time.now + config[:expires_in].to_i
|
|
403
411
|
ctx.context.internal_adapter.create_verification_value(
|
|
404
412
|
identifier: identifier,
|
|
405
413
|
value: "#{code}:0",
|
|
406
|
-
expiresAt:
|
|
414
|
+
expiresAt: expires_at
|
|
407
415
|
)
|
|
408
416
|
end
|
|
409
417
|
|
|
@@ -442,15 +450,5 @@ module BetterAuth
|
|
|
442
450
|
def truthy?(value)
|
|
443
451
|
value == true || value.to_s == "true"
|
|
444
452
|
end
|
|
445
|
-
|
|
446
|
-
def deep_merge_hashes(base, override)
|
|
447
|
-
base.merge(override) do |_key, old_value, new_value|
|
|
448
|
-
if old_value.is_a?(Hash) && new_value.is_a?(Hash)
|
|
449
|
-
deep_merge_hashes(old_value, new_value)
|
|
450
|
-
else
|
|
451
|
-
new_value
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
end
|
|
455
453
|
end
|
|
456
454
|
end
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
return if defined?(BetterAuth::Plugins::SCIM_PLUGIN_IMPLEMENTATION)
|
|
4
|
+
|
|
3
5
|
module BetterAuth
|
|
4
6
|
module Plugins
|
|
5
7
|
module_function
|
|
6
8
|
|
|
7
|
-
def scim(*args)
|
|
8
|
-
|
|
9
|
-
BetterAuth::Plugins.scim(*args)
|
|
10
|
-
rescue LoadError => error
|
|
11
|
-
raise if error.path && error.path != "better_auth/scim"
|
|
12
|
-
|
|
13
|
-
raise LoadError, "BetterAuth::Plugins.scim requires the better_auth-scim gem. Add `gem \"better_auth-scim\"` and `require \"better_auth/scim\"`."
|
|
9
|
+
def scim(*args, &block)
|
|
10
|
+
call_external_plugin!(:scim, *args, implementation_constant: :SCIM_PLUGIN_IMPLEMENTATION, gem_name: "better_auth-scim", entry: "lib/better_auth/scim.rb", &block)
|
|
14
11
|
end
|
|
15
12
|
end
|
|
16
13
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
|
+
require "time"
|
|
4
5
|
|
|
5
6
|
module BetterAuth
|
|
6
7
|
module Plugins
|
|
@@ -8,6 +9,8 @@ module BetterAuth
|
|
|
8
9
|
|
|
9
10
|
SIWE_WALLET_PATTERN = /\A0[xX][a-fA-F0-9]{40}\z/
|
|
10
11
|
SIWE_EMAIL_PATTERN = /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
|
|
12
|
+
SIWE_HEADER_PATTERN = /\A(?:([a-zA-Z][a-zA-Z0-9+.-]*):\/\/)?(\S+) wants you to sign in with your Ethereum account:\z/
|
|
13
|
+
SIWE_FIELD_PATTERN = /\A([A-Za-z ]+): (.*)\z/
|
|
11
14
|
|
|
12
15
|
def siwe(options = {})
|
|
13
16
|
config = normalize_hash(options)
|
|
@@ -16,29 +19,30 @@ module BetterAuth
|
|
|
16
19
|
id: "siwe",
|
|
17
20
|
schema: siwe_schema(config[:schema]),
|
|
18
21
|
endpoints: {
|
|
19
|
-
get_siwe_nonce: get_siwe_nonce_endpoint(config),
|
|
22
|
+
get_siwe_nonce: get_siwe_nonce_endpoint(config, path: "/siwe/nonce", operation_id: "getSiweNonce"),
|
|
23
|
+
get_nonce: get_siwe_nonce_endpoint(config, path: "/siwe/get-nonce", operation_id: "getNonce"),
|
|
20
24
|
verify_siwe_message: verify_siwe_message_endpoint(config)
|
|
21
25
|
},
|
|
22
26
|
options: config
|
|
23
27
|
)
|
|
24
28
|
end
|
|
25
29
|
|
|
26
|
-
def get_siwe_nonce_endpoint(config)
|
|
30
|
+
def get_siwe_nonce_endpoint(config, path:, operation_id:)
|
|
27
31
|
Endpoint.new(
|
|
28
|
-
path:
|
|
32
|
+
path: path,
|
|
29
33
|
method: "POST",
|
|
30
34
|
body_schema: ->(body) { siwe_nonce_body(body) },
|
|
31
35
|
metadata: {
|
|
32
36
|
openapi: {
|
|
33
|
-
operationId:
|
|
37
|
+
operationId: operation_id,
|
|
34
38
|
description: "Generate a nonce for Sign-In with Ethereum",
|
|
35
39
|
requestBody: OpenAPI.json_request_body(
|
|
36
40
|
OpenAPI.object_schema(
|
|
37
41
|
{
|
|
42
|
+
address: {type: "string"},
|
|
38
43
|
walletAddress: {type: "string"},
|
|
39
44
|
chainId: {type: ["number", "string", "null"]}
|
|
40
|
-
}
|
|
41
|
-
required: ["walletAddress"]
|
|
45
|
+
}
|
|
42
46
|
)
|
|
43
47
|
),
|
|
44
48
|
responses: {
|
|
@@ -56,7 +60,7 @@ module BetterAuth
|
|
|
56
60
|
}
|
|
57
61
|
) do |ctx|
|
|
58
62
|
body = normalize_hash(ctx.body)
|
|
59
|
-
wallet_address = siwe_normalize_wallet!(body[:wallet_address])
|
|
63
|
+
wallet_address = siwe_normalize_wallet!(body[:wallet_address] || body[:address])
|
|
60
64
|
chain_id = siwe_chain_id(body[:chain_id])
|
|
61
65
|
nonce_callback = config[:get_nonce]
|
|
62
66
|
raise APIError.new("INTERNAL_SERVER_ERROR", message: "SIWE nonce callback is required") unless nonce_callback.respond_to?(:call)
|
|
@@ -111,21 +115,21 @@ module BetterAuth
|
|
|
111
115
|
body = normalize_hash(ctx.body)
|
|
112
116
|
wallet_address = siwe_normalize_wallet!(body[:wallet_address])
|
|
113
117
|
chain_id = siwe_chain_id(body[:chain_id])
|
|
114
|
-
email = body[:email].to_s
|
|
118
|
+
email = body[:email].to_s.downcase
|
|
115
119
|
anonymous = config.key?(:anonymous) ? config[:anonymous] : true
|
|
116
120
|
raise APIError.new("BAD_REQUEST", message: "Email is required when anonymous is disabled.") if anonymous == false && email.empty?
|
|
117
121
|
raise APIError.new("BAD_REQUEST", message: "Invalid email address") if !email.empty? && !SIWE_EMAIL_PATTERN.match?(email)
|
|
118
122
|
|
|
119
|
-
verification = ctx.context.internal_adapter.
|
|
120
|
-
|
|
123
|
+
verification = ctx.context.internal_adapter.consume_verification_value(siwe_identifier(wallet_address, chain_id))
|
|
124
|
+
unless verification
|
|
121
125
|
raise APIError.new("UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE", message: "Unauthorized: Invalid or expired nonce")
|
|
122
126
|
end
|
|
123
127
|
|
|
128
|
+
siwe_validate_message!(body[:message], config[:domain], wallet_address, chain_id, verification["value"])
|
|
129
|
+
|
|
124
130
|
verified = siwe_verify_message(config, body, wallet_address, chain_id, verification["value"], ctx)
|
|
125
131
|
raise APIError.new("UNAUTHORIZED", message: "Unauthorized: Invalid SIWE signature") unless verified
|
|
126
132
|
|
|
127
|
-
ctx.context.internal_adapter.delete_verification_value(verification["id"])
|
|
128
|
-
|
|
129
133
|
user = siwe_find_user(ctx, wallet_address, chain_id)
|
|
130
134
|
user ||= siwe_create_user(ctx, config, wallet_address, chain_id, email, anonymous)
|
|
131
135
|
siwe_ensure_wallet_and_account(ctx, user, wallet_address, chain_id)
|
|
@@ -188,7 +192,12 @@ module BetterAuth
|
|
|
188
192
|
|
|
189
193
|
def siwe_nonce_body(body)
|
|
190
194
|
data = normalize_hash(body)
|
|
191
|
-
|
|
195
|
+
wallet_address_keys = [:wallet_address, :address].select { |key| data.key?(key) }
|
|
196
|
+
if wallet_address_keys.empty?
|
|
197
|
+
raise APIError.new("BAD_REQUEST", message: "walletAddress or address is required")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
wallet_address_keys.each { |key| siwe_normalize_wallet!(data[key]) }
|
|
192
201
|
data[:chain_id] = siwe_chain_id(data[:chain_id])
|
|
193
202
|
data
|
|
194
203
|
end
|
|
@@ -201,10 +210,11 @@ module BetterAuth
|
|
|
201
210
|
siwe_normalize_wallet!(data[:wallet_address])
|
|
202
211
|
data[:chain_id] = siwe_chain_id(data[:chain_id])
|
|
203
212
|
anonymous = config.key?(:anonymous) ? config[:anonymous] : true
|
|
204
|
-
email = data[:email].to_s
|
|
213
|
+
email = data[:email].to_s.downcase
|
|
205
214
|
raise APIError.new("BAD_REQUEST", message: "Email is required when anonymous is disabled.") if anonymous == false && email.empty?
|
|
206
215
|
raise APIError.new("BAD_REQUEST", message: "Invalid email address") if !email.empty? && !SIWE_EMAIL_PATTERN.match?(email)
|
|
207
216
|
|
|
217
|
+
data[:email] = email unless email.empty?
|
|
208
218
|
data
|
|
209
219
|
end
|
|
210
220
|
|
|
@@ -226,6 +236,101 @@ module BetterAuth
|
|
|
226
236
|
"siwe:#{wallet_address}:#{chain_id}"
|
|
227
237
|
end
|
|
228
238
|
|
|
239
|
+
def siwe_parse_message(message)
|
|
240
|
+
result = {}
|
|
241
|
+
lines = message.to_s.split(/\r?\n/)
|
|
242
|
+
|
|
243
|
+
if (header = SIWE_HEADER_PATTERN.match(lines[0].to_s))
|
|
244
|
+
result[:scheme] = header[1] if header[1]
|
|
245
|
+
result[:domain] = header[2]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
address = lines[1].to_s.strip
|
|
249
|
+
result[:address] = address if SIWE_WALLET_PATTERN.match?(address)
|
|
250
|
+
|
|
251
|
+
lines.each do |line|
|
|
252
|
+
field = SIWE_FIELD_PATTERN.match(line)
|
|
253
|
+
next unless field
|
|
254
|
+
|
|
255
|
+
key = field[1]
|
|
256
|
+
value = field[2]
|
|
257
|
+
case key
|
|
258
|
+
when "URI"
|
|
259
|
+
result[:uri] = value
|
|
260
|
+
when "Version"
|
|
261
|
+
result[:version] = value
|
|
262
|
+
when "Chain ID"
|
|
263
|
+
parsed_chain_id = siwe_parse_message_chain_id(value)
|
|
264
|
+
result[:chain_id] = parsed_chain_id unless parsed_chain_id.nil?
|
|
265
|
+
when "Nonce"
|
|
266
|
+
result[:nonce] = value
|
|
267
|
+
when "Issued At"
|
|
268
|
+
result[:issued_at] = value
|
|
269
|
+
when "Expiration Time"
|
|
270
|
+
result[:expiration_time] = value
|
|
271
|
+
when "Not Before"
|
|
272
|
+
result[:not_before] = value
|
|
273
|
+
when "Request ID"
|
|
274
|
+
result[:request_id] = value
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
result
|
|
279
|
+
rescue
|
|
280
|
+
{}
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def siwe_parse_message_chain_id(value)
|
|
284
|
+
number = Float(value)
|
|
285
|
+
number.to_i if number.finite? && number == number.to_i
|
|
286
|
+
rescue ArgumentError, TypeError, RangeError
|
|
287
|
+
nil
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def siwe_normalize_domain(domain)
|
|
291
|
+
normalized = domain.to_s.strip.downcase.sub(/\A[a-z][a-z0-9+.-]*:\/\//, "")
|
|
292
|
+
path_start = normalized.index("/")
|
|
293
|
+
path_start ? normalized[0...path_start] : normalized
|
|
294
|
+
rescue
|
|
295
|
+
""
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def siwe_validate_message!(message, domain, wallet_address, chain_id, nonce)
|
|
299
|
+
parsed = siwe_parse_message(message)
|
|
300
|
+
matches = parsed[:nonce] == nonce &&
|
|
301
|
+
parsed[:address]&.downcase == wallet_address.downcase &&
|
|
302
|
+
parsed[:chain_id] == chain_id &&
|
|
303
|
+
parsed[:domain] && siwe_normalize_domain(parsed[:domain]) == siwe_normalize_domain(domain)
|
|
304
|
+
|
|
305
|
+
unless matches
|
|
306
|
+
raise siwe_unauthorized_error(
|
|
307
|
+
"UNAUTHORIZED_SIWE_MESSAGE_MISMATCH",
|
|
308
|
+
"Unauthorized: SIWE message does not match the expected nonce, domain, address, or chain ID"
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
now = Time.now
|
|
313
|
+
expiration_time = siwe_parse_message_time(parsed[:expiration_time]) if parsed[:expiration_time]
|
|
314
|
+
if expiration_time && now >= expiration_time
|
|
315
|
+
raise siwe_unauthorized_error("UNAUTHORIZED_SIWE_MESSAGE_EXPIRED", "Unauthorized: SIWE message has expired")
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
not_before = siwe_parse_message_time(parsed[:not_before]) if parsed[:not_before]
|
|
319
|
+
if not_before && now < not_before
|
|
320
|
+
raise siwe_unauthorized_error("UNAUTHORIZED_SIWE_MESSAGE_NOT_YET_VALID", "Unauthorized: SIWE message is not yet valid")
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def siwe_parse_message_time(value)
|
|
325
|
+
Time.parse(value.to_s)
|
|
326
|
+
rescue ArgumentError, TypeError, RangeError
|
|
327
|
+
nil
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def siwe_unauthorized_error(status, message)
|
|
331
|
+
APIError.new("UNAUTHORIZED", code: status, message: message)
|
|
332
|
+
end
|
|
333
|
+
|
|
229
334
|
def siwe_verify_message(config, body, wallet_address, chain_id, nonce, ctx)
|
|
230
335
|
verifier = config[:verify_message]
|
|
231
336
|
raise APIError.new("INTERNAL_SERVER_ERROR", message: "SIWE verify_message callback is required") unless verifier.respond_to?(:call)
|
|
@@ -262,12 +367,17 @@ module BetterAuth
|
|
|
262
367
|
end
|
|
263
368
|
|
|
264
369
|
def siwe_create_user(ctx, config, wallet_address, _chain_id, email, anonymous)
|
|
265
|
-
domain = config[:email_domain_name] || URI.parse(ctx.context.
|
|
370
|
+
domain = config[:email_domain_name] || URI.parse(ctx.context.canonical_base_url).host || ctx.context.canonical_base_url
|
|
266
371
|
lookup = config[:ens_lookup]
|
|
267
372
|
ens = lookup.respond_to?(:call) ? normalize_hash(lookup.call(wallet_address: wallet_address) || {}) : {}
|
|
373
|
+
normalized_email = email.to_s.downcase
|
|
374
|
+
user_email = "#{wallet_address}@#{domain}".downcase
|
|
375
|
+
if anonymous == false && !normalized_email.empty? && !ctx.context.internal_adapter.find_user_by_email(normalized_email)
|
|
376
|
+
user_email = normalized_email
|
|
377
|
+
end
|
|
268
378
|
ctx.context.internal_adapter.create_user(
|
|
269
379
|
name: ens[:name] || wallet_address,
|
|
270
|
-
email:
|
|
380
|
+
email: user_email,
|
|
271
381
|
image: ens[:avatar] || "",
|
|
272
382
|
context: ctx
|
|
273
383
|
)
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
return if defined?(BetterAuth::Plugins::SSO_PLUGIN_IMPLEMENTATION)
|
|
4
|
+
|
|
3
5
|
module BetterAuth
|
|
4
6
|
module Plugins
|
|
5
7
|
module_function
|
|
6
8
|
|
|
7
|
-
def sso(*args)
|
|
8
|
-
|
|
9
|
-
BetterAuth::Plugins.sso(*args)
|
|
10
|
-
rescue LoadError => error
|
|
11
|
-
raise if error.path && error.path != "better_auth/sso"
|
|
12
|
-
|
|
13
|
-
raise LoadError, "BetterAuth::Plugins.sso requires the better_auth-sso gem. Add `gem \"better_auth-sso\"` and `require \"better_auth/sso\"`."
|
|
9
|
+
def sso(*args, &block)
|
|
10
|
+
call_external_plugin!(:sso, *args, implementation_constant: :SSO_PLUGIN_IMPLEMENTATION, gem_name: "better_auth-sso", entry: "lib/better_auth/sso.rb", &block)
|
|
14
11
|
end
|
|
15
12
|
end
|
|
16
13
|
end
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
return if defined?(BetterAuth::Plugins::STRIPE_PLUGIN_IMPLEMENTATION)
|
|
4
|
+
|
|
3
5
|
module BetterAuth
|
|
4
6
|
module Plugins
|
|
5
7
|
module_function
|
|
6
8
|
|
|
7
|
-
def stripe(*args)
|
|
8
|
-
|
|
9
|
-
BetterAuth::Plugins.stripe(*args)
|
|
10
|
-
rescue LoadError => error
|
|
11
|
-
raise if error.path && error.path != "better_auth/stripe"
|
|
12
|
-
|
|
13
|
-
raise LoadError, "BetterAuth::Plugins.stripe requires the better_auth-stripe gem. Add `gem \"better_auth-stripe\"` and `require \"better_auth/stripe\"`."
|
|
9
|
+
def stripe(*args, &block)
|
|
10
|
+
call_external_plugin!(:stripe, *args, implementation_constant: :STRIPE_PLUGIN_IMPLEMENTATION, gem_name: "better_auth-stripe", entry: "lib/better_auth/stripe.rb", &block)
|
|
14
11
|
end
|
|
15
12
|
end
|
|
16
13
|
end
|