better_auth 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +5 -3
  4. data/lib/better_auth/adapters/internal_adapter.rb +168 -18
  5. data/lib/better_auth/adapters/memory.rb +4 -1
  6. data/lib/better_auth/adapters/mongodb.rb +5 -365
  7. data/lib/better_auth/adapters/sql.rb +17 -1
  8. data/lib/better_auth/api.rb +1 -1
  9. data/lib/better_auth/context.rb +2 -1
  10. data/lib/better_auth/plugin.rb +14 -1
  11. data/lib/better_auth/plugins/oauth_protocol.rb +403 -57
  12. data/lib/better_auth/plugins/organization.rb +5 -0
  13. data/lib/better_auth/rate_limiter.rb +19 -2
  14. data/lib/better_auth/router.rb +14 -1
  15. data/lib/better_auth/routes/email_verification.rb +5 -2
  16. data/lib/better_auth/routes/password.rb +19 -0
  17. data/lib/better_auth/routes/session.rb +27 -4
  18. data/lib/better_auth/routes/sign_in.rb +1 -1
  19. data/lib/better_auth/routes/sign_up.rb +52 -1
  20. data/lib/better_auth/routes/social.rb +201 -22
  21. data/lib/better_auth/routes/user.rb +14 -2
  22. data/lib/better_auth/schema/sql.rb +11 -0
  23. data/lib/better_auth/schema.rb +16 -0
  24. data/lib/better_auth/social_providers/apple.rb +44 -8
  25. data/lib/better_auth/social_providers/atlassian.rb +32 -0
  26. data/lib/better_auth/social_providers/base.rb +262 -4
  27. data/lib/better_auth/social_providers/cognito.rb +32 -0
  28. data/lib/better_auth/social_providers/discord.rb +27 -5
  29. data/lib/better_auth/social_providers/dropbox.rb +33 -0
  30. data/lib/better_auth/social_providers/facebook.rb +35 -0
  31. data/lib/better_auth/social_providers/figma.rb +31 -0
  32. data/lib/better_auth/social_providers/github.rb +21 -6
  33. data/lib/better_auth/social_providers/gitlab.rb +16 -3
  34. data/lib/better_auth/social_providers/google.rb +38 -13
  35. data/lib/better_auth/social_providers/huggingface.rb +31 -0
  36. data/lib/better_auth/social_providers/kakao.rb +32 -0
  37. data/lib/better_auth/social_providers/kick.rb +32 -0
  38. data/lib/better_auth/social_providers/line.rb +33 -0
  39. data/lib/better_auth/social_providers/linear.rb +44 -0
  40. data/lib/better_auth/social_providers/linkedin.rb +30 -0
  41. data/lib/better_auth/social_providers/microsoft_entra_id.rb +79 -7
  42. data/lib/better_auth/social_providers/naver.rb +31 -0
  43. data/lib/better_auth/social_providers/notion.rb +33 -0
  44. data/lib/better_auth/social_providers/paybin.rb +31 -0
  45. data/lib/better_auth/social_providers/paypal.rb +36 -0
  46. data/lib/better_auth/social_providers/polar.rb +31 -0
  47. data/lib/better_auth/social_providers/railway.rb +49 -0
  48. data/lib/better_auth/social_providers/reddit.rb +32 -0
  49. data/lib/better_auth/social_providers/roblox.rb +31 -0
  50. data/lib/better_auth/social_providers/salesforce.rb +38 -0
  51. data/lib/better_auth/social_providers/slack.rb +30 -0
  52. data/lib/better_auth/social_providers/spotify.rb +31 -0
  53. data/lib/better_auth/social_providers/tiktok.rb +35 -0
  54. data/lib/better_auth/social_providers/twitch.rb +39 -0
  55. data/lib/better_auth/social_providers/twitter.rb +32 -0
  56. data/lib/better_auth/social_providers/vercel.rb +47 -0
  57. data/lib/better_auth/social_providers/vk.rb +34 -0
  58. data/lib/better_auth/social_providers/wechat.rb +104 -0
  59. data/lib/better_auth/social_providers/zoom.rb +31 -0
  60. data/lib/better_auth/social_providers.rb +29 -0
  61. data/lib/better_auth/version.rb +1 -1
  62. data/lib/better_auth.rb +0 -1
  63. metadata +30 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5e95c51ce68259fcccbbfabe1bd9ffa5383ce4c098161f1c2d54da76137fa0a
4
- data.tar.gz: 7c81ade6292ced9b98c1c411c320e4ad6e05734ea891145f75a9205d6e4fdea0
3
+ metadata.gz: df93cdae06059d52fa2c3d35c5b173aba9025d5ac46bda0b93a7a8abc5045f40
4
+ data.tar.gz: adec802dade2610da15329266c91dcd46aecb8642165c29e364ce7db2829d217
5
5
  SHA512:
6
- metadata.gz: 4b7f5c635ce16aa7be7f3ea42c5fa03304b7c682821c8a82e55c81754ba330a88aba350f0a6ed59b171333766ba6a973c4a2d15cf3d46debeae42ddd59b602c5
7
- data.tar.gz: 524ce3bfb3f023bcf057154fde0e88e0555753016d1297d67d40f2db6de6cf5516b0fcb8b513fd260cd284b38722eeffe9a02784d3d08a58cd5811cce23a1d39
6
+ metadata.gz: f46ac8a5cf79a859a417e2cbe60f000c290d014975fb3ce910c49b6c1cd455b2123e436a42a323dd530b38096a4ebc18a1f206c97f0ef6624378c9eb051d37e3
7
+ data.tar.gz: '0469ddcdcad97a7b1b58e3ddc0ef8d54c7469a1e0411546367f829291ab5664fc0b4685d460d8d328fe6ef67543908060d33eb961a638121930154ca31a4cfaf'
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-04-29
11
+
12
+ ### Added
13
+
14
+ - Added upstream-parity social provider support, including provider-specific authorization, token, profile, refresh, and revocation behavior for the expanded provider set.
15
+ - Added OAuth/OIDC protocol hardening for authorization, callback, discovery, metadata, token, and userinfo flows.
16
+ - Added upstream v1.6.9 parity coverage for schema generation, adapter behavior, plugin hooks, session handling, and account/user route edge cases.
17
+
18
+ ### Changed
19
+
20
+ - Extracted MongoDB adapter support behind the external `better_auth-mongo-adapter` shim while preserving compatibility for existing adapter configuration.
21
+ - Updated auth routes, router behavior, rate limiting, password and email-verification flows, and schema metadata to match upstream semantics more closely.
22
+
23
+ ### Fixed
24
+
25
+ - Fixed social provider edge cases, magic-link expiration behavior, adapter value coercion, and callback/session handling across Rack integrations.
26
+
10
27
  ## [0.1.1] - 2026-03-22
11
28
 
12
29
  ### Fixed
data/README.md CHANGED
@@ -90,7 +90,7 @@ Custom Better Auth-style password callbacks are still supported through `email_a
90
90
 
91
91
  ### Database Adapters
92
92
 
93
- The core gem ships framework-agnostic adapters for memory, PostgreSQL, MySQL, SQLite, MongoDB, and MSSQL. Driver gems are loaded only when their adapter is instantiated.
93
+ The core gem ships framework-agnostic adapters for memory, PostgreSQL, MySQL, SQLite, and MSSQL. Driver gems are loaded only when their adapter is instantiated. MongoDB support lives in the external `better_auth-mongo-adapter` package so apps that do not use MongoDB do not install the Mongo driver.
94
94
 
95
95
  ```ruby
96
96
  auth = BetterAuth.auth(
@@ -100,6 +100,8 @@ auth = BetterAuth.auth(
100
100
  ```
101
101
 
102
102
  ```ruby
103
+ require "better_auth/mongo_adapter"
104
+
103
105
  auth = BetterAuth.auth(
104
106
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
105
107
  database: BetterAuth::Adapters::MongoDB.new(
@@ -155,7 +157,7 @@ export const authClient = createAuthClient({
155
157
  Add to your Gemfile:
156
158
 
157
159
  ```ruby
158
- gem 'better_auth', require: 'better_auth/rails'
160
+ gem "better_auth-rails"
159
161
  ```
160
162
 
161
163
  Then in your ApplicationController:
@@ -170,7 +172,7 @@ Now you have access to `current_user` and authentication methods:
170
172
 
171
173
  ```ruby
172
174
  class PostsController < ApplicationController
173
- before_action :authenticate_user!
175
+ before_action :require_authentication
174
176
 
175
177
  def index
176
178
  @posts = current_user.posts
@@ -74,28 +74,31 @@ module BetterAuth
74
74
  "userId" => user_id,
75
75
  "token" => token
76
76
  }.merge(timestamps)
77
+ base["id"] = generated_id if secondary_storage
77
78
  data = override_all ? base.merge(override) : override.merge(base)
78
79
 
79
80
  custom = secondary_storage && lambda do |session_data|
80
81
  actual_session = apply_schema_create("session", session_data)
81
82
  store_session(actual_session)
83
+ adapter.create(model: "session", data: actual_session, force_allow_id: true) if options.session[:store_session_in_database]
82
84
  actual_session
83
85
  end
84
- execute_main = !secondary_storage || options.session[:store_session_in_database]
85
- created = hooks.create(data, "session", custom: custom, context: context)
86
- adapter.create(model: "session", data: data, force_allow_id: true) if secondary_storage && execute_main
87
- created
86
+ hooks.create(data, "session", custom: custom, context: context)
88
87
  end
89
88
 
90
89
  def find_session(token)
91
90
  if secondary_storage
92
91
  data = parse_storage(secondary_storage.get(token))
93
- return nil unless data
92
+ unless data
93
+ return nil unless options.session[:store_session_in_database] && !options.session[:preserve_session_in_database]
94
+ end
94
95
 
95
- return {
96
- session: normalize_session_dates(data["session"]),
97
- user: normalize_user_dates(data["user"])
98
- }
96
+ if data
97
+ return {
98
+ session: normalize_session_dates(data["session"]),
99
+ user: normalize_user_dates(data["user"])
100
+ }
101
+ end
99
102
  end
100
103
 
101
104
  found = find_session_with_user(token)
@@ -113,7 +116,9 @@ module BetterAuth
113
116
  data = stringify_keys(session)
114
117
  if secondary_storage
115
118
  return hooks.update(data, [{field: "token", value: token}], "session", custom: lambda { |actual_data|
116
- update_stored_session(token, actual_data)
119
+ stored = update_stored_session(token, actual_data)
120
+ db = adapter.update(model: "session", where: [{field: "token", value: token}], update: actual_data) if options.session[:store_session_in_database]
121
+ db || stored
117
122
  })
118
123
  end
119
124
 
@@ -226,30 +231,93 @@ module BetterAuth
226
231
  end
227
232
 
228
233
  def create_verification_value(data)
229
- hooks.create(timestamps.merge(stringify_keys(data)), "verification")
234
+ payload = timestamps.merge(stringify_keys(data))
235
+ stored_identifier = processed_verification_identifier(payload.fetch("identifier"))
236
+ payload["identifier"] = stored_identifier
237
+
238
+ custom = secondary_storage && lambda do |verification_data|
239
+ actual = apply_schema_create("verification", verification_data)
240
+ actual["id"] ||= generated_id
241
+ store_verification(actual)
242
+ adapter.create(model: "verification", data: actual, force_allow_id: true) if verification_store_in_database?
243
+ actual
244
+ end
245
+
246
+ hooks.create(payload, "verification", custom: custom)
230
247
  end
231
248
 
232
249
  def find_verification_value(identifier)
250
+ stored_identifier = processed_verification_identifier(identifier)
251
+ storage_option = verification_storage_option(identifier)
252
+ if secondary_storage
253
+ cached = read_verification(stored_identifier)
254
+ cached ||= read_verification(identifier) if storage_option && storage_option.to_s != "plain"
255
+ return cached if cached
256
+ return nil unless verification_store_in_database?
257
+ end
258
+
233
259
  values = adapter.find_many(
234
260
  model: "verification",
235
- where: [{field: "identifier", value: identifier}],
261
+ where: [{field: "identifier", value: stored_identifier}],
236
262
  sort_by: {field: "createdAt", direction: "desc"},
237
263
  limit: 1
238
264
  )
265
+ if values.empty? && storage_option && storage_option.to_s != "plain"
266
+ values = adapter.find_many(
267
+ model: "verification",
268
+ where: [{field: "identifier", value: identifier}],
269
+ sort_by: {field: "createdAt", direction: "desc"},
270
+ limit: 1
271
+ )
272
+ end
239
273
  hooks.delete_many([{field: "expiresAt", value: Time.now, operator: "lt"}], "verification") unless options.verification[:disable_cleanup]
240
274
  values.first
241
275
  end
242
276
 
243
277
  def delete_verification_value(id)
278
+ if secondary_storage
279
+ stored_identifier = secondary_storage.get(verification_id_key(id))
280
+ if stored_identifier
281
+ secondary_storage.delete(verification_key(stored_identifier))
282
+ secondary_storage.delete(verification_id_key(id))
283
+ return nil unless verification_store_in_database?
284
+ elsif !verification_store_in_database?
285
+ return nil
286
+ end
287
+ end
288
+
244
289
  hooks.delete([{field: "id", value: id}], "verification")
245
290
  end
246
291
 
247
292
  def delete_verification_by_identifier(identifier)
248
- hooks.delete([{field: "identifier", value: identifier}], "verification")
293
+ stored_identifier = processed_verification_identifier(identifier)
294
+ if secondary_storage
295
+ cached = read_verification(stored_identifier)
296
+ secondary_storage.delete(verification_key(stored_identifier))
297
+ secondary_storage.delete(verification_id_key(cached["id"])) if cached && cached["id"]
298
+ return nil unless verification_store_in_database?
299
+ end
300
+
301
+ hooks.delete([{field: "identifier", value: stored_identifier}], "verification")
249
302
  end
250
303
 
251
304
  def update_verification_value(id, data)
252
- hooks.update(stringify_keys(data), [{field: "id", value: id}], "verification")
305
+ update = stringify_keys(data)
306
+ if secondary_storage
307
+ stored_identifier = secondary_storage.get(verification_id_key(id))
308
+ if stored_identifier
309
+ cached = read_verification(stored_identifier)
310
+ if cached
311
+ updated = cached.merge(update)
312
+ store_verification(updated)
313
+ return updated unless verification_store_in_database?
314
+ end
315
+ elsif !verification_store_in_database?
316
+ return nil
317
+ end
318
+ end
319
+
320
+ hooks.update(update, [{field: "id", value: id}], "verification")
253
321
  end
254
322
 
255
323
  private
@@ -285,6 +353,14 @@ module BetterAuth
285
353
  {"createdAt" => now, "updatedAt" => now}
286
354
  end
287
355
 
356
+ def generated_id
357
+ generator = options.advanced.dig(:database, :generate_id)
358
+ return generator.call.to_s if generator.respond_to?(:call)
359
+ return SecureRandom.uuid if generator == "uuid"
360
+
361
+ SecureRandom.hex(16)
362
+ end
363
+
288
364
  def stringify_keys(data)
289
365
  data.each_with_object({}) do |(key, value), result|
290
366
  result[Schema.storage_key(key)] = value
@@ -295,6 +371,8 @@ module BetterAuth
295
371
  fields = Schema.auth_tables(options)[model]&.fetch(:fields)
296
372
  fields ||= session_additional_fields if model == "session"
297
373
  output = stringify_keys(data)
374
+ return output unless fields
375
+
298
376
  fields.each do |field, attributes|
299
377
  unless output.key?(field)
300
378
  if attributes.key?(:default_value)
@@ -334,7 +412,8 @@ module BetterAuth
334
412
  .push({"token" => session["token"], "expiresAt" => expires_ms})
335
413
  .sort_by { |entry| entry["expiresAt"] }
336
414
  write_active_sessions(session["userId"], entries)
337
- secondary_storage.set(session["token"], JSON.generate({session: session, user: user}), ttl(expires_ms))
415
+ ttl_seconds = ttl(expires_ms)
416
+ secondary_storage.set(session["token"], JSON.generate({session: session, user: user}), ttl_seconds) if ttl_seconds.positive?
338
417
  end
339
418
 
340
419
  def update_stored_session(token, data)
@@ -345,7 +424,12 @@ module BetterAuth
345
424
  merged["expiresAt"] = normalize_time(merged["expiresAt"])
346
425
  merged["createdAt"] = normalize_time(merged["createdAt"])
347
426
  merged["updatedAt"] = normalize_time(merged["updatedAt"])
348
- secondary_storage.set(token, JSON.generate({session: merged, user: parsed["user"]}), ttl(millis(merged["expiresAt"])))
427
+ ttl_seconds = ttl(millis(merged["expiresAt"]))
428
+ if ttl_seconds.positive?
429
+ secondary_storage.set(token, JSON.generate({session: merged, user: parsed["user"]}), ttl_seconds)
430
+ else
431
+ secondary_storage.delete(token)
432
+ end
349
433
  entries = active_session_entries(merged["userId"])
350
434
  .reject { |entry| entry["token"] == token || entry["expiresAt"].to_i <= current_millis }
351
435
  .push({"token" => token, "expiresAt" => millis(merged["expiresAt"])})
@@ -369,7 +453,7 @@ module BetterAuth
369
453
  raw = secondary_storage.get(active_key(user_id))
370
454
  Array(parse_storage(raw)).map do |entry|
371
455
  entry.transform_keys(&:to_s)
372
- end
456
+ end.uniq { |entry| entry["token"] }
373
457
  end
374
458
 
375
459
  def write_active_sessions(user_id, entries)
@@ -377,7 +461,12 @@ module BetterAuth
377
461
  if future.empty?
378
462
  secondary_storage.delete(active_key(user_id))
379
463
  else
380
- secondary_storage.set(active_key(user_id), JSON.generate(future), ttl(future.last["expiresAt"]))
464
+ ttl_seconds = ttl(future.last["expiresAt"])
465
+ if ttl_seconds.positive?
466
+ secondary_storage.set(active_key(user_id), JSON.generate(future), ttl_seconds)
467
+ else
468
+ secondary_storage.delete(active_key(user_id))
469
+ end
381
470
  end
382
471
  end
383
472
 
@@ -415,6 +504,67 @@ module BetterAuth
415
504
  )
416
505
  end
417
506
 
507
+ def normalize_verification_dates(verification)
508
+ return nil unless verification
509
+
510
+ verification.transform_keys(&:to_s).merge(
511
+ "expiresAt" => normalize_time(verification["expiresAt"] || verification[:expiresAt]),
512
+ "createdAt" => normalize_time(verification["createdAt"] || verification[:createdAt]),
513
+ "updatedAt" => normalize_time(verification["updatedAt"] || verification[:updatedAt])
514
+ )
515
+ end
516
+
517
+ def store_verification(verification)
518
+ normalized = normalize_verification_dates(verification)
519
+ ttl_seconds = ttl(millis(normalized["expiresAt"]))
520
+ return normalized unless ttl_seconds.positive?
521
+
522
+ secondary_storage.set(verification_key(normalized["identifier"]), JSON.generate(normalized), ttl_seconds)
523
+ secondary_storage.set(verification_id_key(normalized["id"]), normalized["identifier"], ttl_seconds) if normalized["id"]
524
+ normalized
525
+ end
526
+
527
+ def read_verification(identifier)
528
+ normalize_verification_dates(parse_storage(secondary_storage.get(verification_key(identifier))))
529
+ end
530
+
531
+ def verification_key(identifier)
532
+ "verification:#{identifier}"
533
+ end
534
+
535
+ def verification_id_key(id)
536
+ "verification-id:#{id}"
537
+ end
538
+
539
+ def verification_store_in_database?
540
+ !!options.verification[:store_in_database]
541
+ end
542
+
543
+ def processed_verification_identifier(identifier)
544
+ option = verification_storage_option(identifier)
545
+ return identifier.to_s if option.nil? || option.to_s == "plain"
546
+ return Crypto.sha256(identifier.to_s, encoding: :base64url) if option.to_s == "hashed"
547
+ return option[:hash].call(identifier.to_s).to_s if option.is_a?(Hash) && option[:hash].respond_to?(:call)
548
+ return option["hash"].call(identifier.to_s).to_s if option.is_a?(Hash) && option["hash"].respond_to?(:call)
549
+
550
+ identifier.to_s
551
+ end
552
+
553
+ def verification_storage_option(identifier)
554
+ config = options.verification[:store_identifier]
555
+ return nil unless config
556
+
557
+ if config.is_a?(Hash) && (config.key?(:default) || config.key?("default"))
558
+ overrides = config[:overrides] || config["overrides"] || {}
559
+ overrides.each do |prefix, option|
560
+ return option if identifier.to_s.start_with?(prefix.to_s)
561
+ end
562
+ return config[:default] || config["default"]
563
+ end
564
+
565
+ config
566
+ end
567
+
418
568
  def normalize_time(value)
419
569
  return value if value.is_a?(Time)
420
570
  return Time.at(value / 1000.0) if value.is_a?(Integer) && value > 10_000_000_000
@@ -225,7 +225,10 @@ module BetterAuth
225
225
  end
226
226
 
227
227
  def fetch_key(hash, key)
228
- hash[key] || hash[key.to_s] || hash[Schema.storage_key(key)] || hash[Schema.storage_key(key).to_sym]
228
+ [key, key.to_s, Schema.storage_key(key), Schema.storage_key(key).to_sym].each do |candidate|
229
+ return hash[candidate] if hash.key?(candidate)
230
+ end
231
+ nil
229
232
  end
230
233
  end
231
234
  end