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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require "time"
|
|
6
|
+
require "monitor"
|
|
6
7
|
|
|
7
8
|
module BetterAuth
|
|
8
9
|
module Adapters
|
|
@@ -13,6 +14,8 @@ module BetterAuth
|
|
|
13
14
|
@adapter = adapter
|
|
14
15
|
@options = options
|
|
15
16
|
@hooks = DatabaseHooks.new(adapter, options)
|
|
17
|
+
@verification_locks_guard = Mutex.new
|
|
18
|
+
@verification_locks = {}
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def create_oauth_user(user, account, context: nil)
|
|
@@ -63,7 +66,7 @@ module BetterAuth
|
|
|
63
66
|
return false if deleted == false
|
|
64
67
|
|
|
65
68
|
hooks.delete_many([{field: "userId", value: user_id}], "account")
|
|
66
|
-
|
|
69
|
+
delete_user_sessions(user_id)
|
|
67
70
|
deleted
|
|
68
71
|
end
|
|
69
72
|
|
|
@@ -143,20 +146,24 @@ module BetterAuth
|
|
|
143
146
|
hooks.delete([{field: "token", value: token}], "session")
|
|
144
147
|
end
|
|
145
148
|
|
|
146
|
-
def
|
|
149
|
+
def delete_user_sessions(user_id)
|
|
147
150
|
if secondary_storage
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
active_session_entries(user_id).each { |entry| secondary_storage.delete(entry["token"]) }
|
|
152
|
+
secondary_storage.delete(active_key(user_id))
|
|
153
|
+
return if !options.session[:store_session_in_database] || options.session[:preserve_session_in_database]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
hooks.delete_many([{field: "userId", value: user_id}], "session")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def delete_sessions(tokens)
|
|
160
|
+
session_tokens = Array(tokens)
|
|
161
|
+
if secondary_storage
|
|
162
|
+
session_tokens.each { |token| secondary_storage.delete(token) }
|
|
154
163
|
return if !options.session[:store_session_in_database] || options.session[:preserve_session_in_database]
|
|
155
164
|
end
|
|
156
165
|
|
|
157
|
-
|
|
158
|
-
operator = user_id_or_tokens.is_a?(Array) ? "in" : nil
|
|
159
|
-
hooks.delete_many([{field: field, value: user_id_or_tokens, operator: operator}], "session")
|
|
166
|
+
hooks.delete_many([{field: "token", value: session_tokens, operator: "in"}], "session")
|
|
160
167
|
end
|
|
161
168
|
|
|
162
169
|
def delete_accounts(user_id)
|
|
@@ -167,6 +174,26 @@ module BetterAuth
|
|
|
167
174
|
hooks.delete([{field: "id", value: account_id}], "account")
|
|
168
175
|
end
|
|
169
176
|
|
|
177
|
+
# Removes access established before control of an email address was proven.
|
|
178
|
+
# The user is intentionally left unverified until the caller completes the
|
|
179
|
+
# proof transition and may safely mint a replacement session.
|
|
180
|
+
def revoke_unproven_account_access(user_id)
|
|
181
|
+
adapter.transaction do
|
|
182
|
+
user = find_user_by_id(user_id)
|
|
183
|
+
next unless user && !user["emailVerified"]
|
|
184
|
+
|
|
185
|
+
find_accounts(user_id).each do |account|
|
|
186
|
+
next unless account["providerId"] == "credential"
|
|
187
|
+
|
|
188
|
+
deleted = delete_account(account.fetch("id"))
|
|
189
|
+
raise Error, "Credential account revocation was vetoed" if deleted == false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
deleted_sessions = delete_user_sessions(user_id)
|
|
193
|
+
raise Error, "Session revocation was vetoed" if deleted_sessions == false
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
170
197
|
def find_oauth_user(email, account_id, provider_id)
|
|
171
198
|
account = find_account_with_user(account_id, provider_id)
|
|
172
199
|
if account
|
|
@@ -277,6 +304,56 @@ module BetterAuth
|
|
|
277
304
|
values.first
|
|
278
305
|
end
|
|
279
306
|
|
|
307
|
+
def consume_verification_value(identifier)
|
|
308
|
+
identifiers = verification_identifiers(identifier)
|
|
309
|
+
consumed = if secondary_storage && !verification_store_in_database?
|
|
310
|
+
consume_secondary_verification(identifiers)
|
|
311
|
+
else
|
|
312
|
+
consume_database_verification(identifiers)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
invalidate_secondary_verification(identifiers, consumed) if consumed && secondary_storage && verification_store_in_database?
|
|
316
|
+
return nil unless consumed
|
|
317
|
+
|
|
318
|
+
expires_at = normalize_time(consumed["expiresAt"] || consumed[:expiresAt])
|
|
319
|
+
(expires_at > Time.now) ? normalize_verification_dates(consumed) : nil
|
|
320
|
+
rescue ArgumentError, TypeError
|
|
321
|
+
nil
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def reserve_verification_value(data)
|
|
325
|
+
payload = stringify_keys(data)
|
|
326
|
+
identifier = payload.fetch("identifier")
|
|
327
|
+
stored_identifier = processed_verification_identifier(identifier)
|
|
328
|
+
reservation_id = Crypto.sha256("reserve:#{identifier}", encoding: :base64url)
|
|
329
|
+
verification = timestamps.merge(
|
|
330
|
+
"id" => reservation_id,
|
|
331
|
+
"identifier" => stored_identifier,
|
|
332
|
+
"value" => payload.fetch("value"),
|
|
333
|
+
"expiresAt" => normalize_time(payload.fetch("expiresAt"))
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if secondary_storage && !verification_store_in_database?
|
|
337
|
+
return reserve_secondary_verification(verification)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
adapter.consume_one(
|
|
341
|
+
model: "verification",
|
|
342
|
+
where: [
|
|
343
|
+
{field: "id", value: reservation_id},
|
|
344
|
+
{field: "expiresAt", value: Time.now, operator: "lt"}
|
|
345
|
+
]
|
|
346
|
+
)
|
|
347
|
+
created = adapter.create_if_absent(
|
|
348
|
+
model: "verification",
|
|
349
|
+
data: verification,
|
|
350
|
+
conflict_field: "id",
|
|
351
|
+
force_allow_id: true
|
|
352
|
+
)
|
|
353
|
+
store_reserved_verification(verification) if created && secondary_storage
|
|
354
|
+
created
|
|
355
|
+
end
|
|
356
|
+
|
|
280
357
|
def delete_verification_value(id)
|
|
281
358
|
if secondary_storage
|
|
282
359
|
stored_identifier = secondary_storage.get(verification_id_key(id))
|
|
@@ -325,6 +402,106 @@ module BetterAuth
|
|
|
325
402
|
|
|
326
403
|
private
|
|
327
404
|
|
|
405
|
+
def verification_identifiers(identifier)
|
|
406
|
+
stored_identifier = processed_verification_identifier(identifier)
|
|
407
|
+
storage_option = verification_storage_option(identifier)
|
|
408
|
+
if storage_option && storage_option.to_s != "plain" && stored_identifier != identifier.to_s
|
|
409
|
+
[stored_identifier, identifier.to_s]
|
|
410
|
+
else
|
|
411
|
+
[stored_identifier]
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def consume_secondary_verification(identifiers)
|
|
416
|
+
identifiers.each do |stored_identifier|
|
|
417
|
+
key = verification_key(stored_identifier)
|
|
418
|
+
raw = if secondary_storage.respond_to?(:get_and_delete)
|
|
419
|
+
secondary_storage.get_and_delete(key)
|
|
420
|
+
elsif secondary_storage.respond_to?(:getAndDelete)
|
|
421
|
+
secondary_storage.getAndDelete(key)
|
|
422
|
+
else
|
|
423
|
+
raise Error, "Secondary storage must implement atomic `get_and_delete`/`getAndDelete`, or verification values must be database-backed"
|
|
424
|
+
end
|
|
425
|
+
consumed = begin
|
|
426
|
+
parsed = parse_storage(raw)
|
|
427
|
+
parsed ? normalize_verification_dates(parsed) : nil
|
|
428
|
+
rescue ArgumentError, TypeError
|
|
429
|
+
nil
|
|
430
|
+
end
|
|
431
|
+
next unless consumed
|
|
432
|
+
|
|
433
|
+
invalidate_secondary_verification(identifiers, consumed)
|
|
434
|
+
return consumed
|
|
435
|
+
end
|
|
436
|
+
nil
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def reserve_secondary_verification(verification)
|
|
440
|
+
key = verification_key(verification.fetch("identifier"))
|
|
441
|
+
ttl_seconds = ttl(millis(verification.fetch("expiresAt")))
|
|
442
|
+
return false unless ttl_seconds.positive?
|
|
443
|
+
|
|
444
|
+
value = JSON.generate(normalize_verification_dates(verification))
|
|
445
|
+
created = if secondary_storage.respond_to?(:set_if_absent)
|
|
446
|
+
secondary_storage.set_if_absent(key, value, ttl_seconds)
|
|
447
|
+
elsif secondary_storage.respond_to?(:setIfAbsent)
|
|
448
|
+
secondary_storage.setIfAbsent(key, value, ttl_seconds)
|
|
449
|
+
else
|
|
450
|
+
raise Error, "Secondary storage must implement atomic `set_if_absent`/`setIfAbsent`, or verification values must be database-backed"
|
|
451
|
+
end
|
|
452
|
+
return false unless created
|
|
453
|
+
true
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def consume_database_verification(identifiers)
|
|
457
|
+
identifiers.each do |stored_identifier|
|
|
458
|
+
consumed = with_verification_lock(verification_key(stored_identifier)) do
|
|
459
|
+
adapter.transaction do |transaction_adapter|
|
|
460
|
+
where = [{field: "identifier", value: stored_identifier}]
|
|
461
|
+
latest = transaction_adapter.find_many(
|
|
462
|
+
model: "verification",
|
|
463
|
+
where: where,
|
|
464
|
+
sort_by: {field: "createdAt", direction: "desc"},
|
|
465
|
+
limit: 1
|
|
466
|
+
).first
|
|
467
|
+
next nil unless latest
|
|
468
|
+
next nil if verification_delete_vetoed?(latest)
|
|
469
|
+
|
|
470
|
+
row = transaction_adapter.consume_one(
|
|
471
|
+
model: "verification",
|
|
472
|
+
where: [{field: "id", value: latest.fetch("id")}]
|
|
473
|
+
)
|
|
474
|
+
next nil unless row
|
|
475
|
+
|
|
476
|
+
transaction_adapter.delete_many(model: "verification", where: where)
|
|
477
|
+
run_verification_delete_after(row)
|
|
478
|
+
row
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
return consumed if consumed
|
|
482
|
+
end
|
|
483
|
+
nil
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def invalidate_secondary_verification(identifiers, consumed)
|
|
487
|
+
identifiers.each { |stored_identifier| secondary_storage.delete(verification_key(stored_identifier)) }
|
|
488
|
+
id = consumed && (consumed["id"] || consumed[:id])
|
|
489
|
+
secondary_storage.delete(verification_id_key(id)) if id
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def verification_delete_vetoed?(entity)
|
|
493
|
+
hooks.send(:before_hooks, "verification", :delete).any? { |hook| hook.call(entity, nil) == false }
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def run_verification_delete_after(entity)
|
|
497
|
+
hooks.send(:after_hooks, "verification", :delete).each { |hook| hook.call(entity, nil) }
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def with_verification_lock(key)
|
|
501
|
+
lock = @verification_locks_guard.synchronize { @verification_locks[key] ||= Monitor.new }
|
|
502
|
+
lock.synchronize { yield }
|
|
503
|
+
end
|
|
504
|
+
|
|
328
505
|
def secondary_storage
|
|
329
506
|
options.secondary_storage
|
|
330
507
|
end
|
|
@@ -527,6 +704,15 @@ module BetterAuth
|
|
|
527
704
|
normalized
|
|
528
705
|
end
|
|
529
706
|
|
|
707
|
+
def store_reserved_verification(verification)
|
|
708
|
+
normalized = normalize_verification_dates(verification)
|
|
709
|
+
ttl_seconds = ttl(millis(normalized["expiresAt"]))
|
|
710
|
+
return normalized unless ttl_seconds.positive?
|
|
711
|
+
|
|
712
|
+
secondary_storage.set(verification_key(normalized["identifier"]), JSON.generate(normalized), ttl_seconds)
|
|
713
|
+
normalized
|
|
714
|
+
end
|
|
715
|
+
|
|
530
716
|
def read_verification(identifier)
|
|
531
717
|
normalize_verification_dates(parse_storage(secondary_storage.get(verification_key(identifier))))
|
|
532
718
|
end
|
|
@@ -58,6 +58,16 @@ module BetterAuth
|
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
def join_limit(config)
|
|
63
|
+
value = config[:limit]
|
|
64
|
+
return 100 if value.nil?
|
|
65
|
+
|
|
66
|
+
parsed = Integer(value)
|
|
67
|
+
parsed.positive? ? parsed : 100
|
|
68
|
+
rescue ArgumentError, TypeError
|
|
69
|
+
100
|
|
70
|
+
end
|
|
61
71
|
end
|
|
62
72
|
end
|
|
63
73
|
end
|