better_auth 0.3.0 → 0.5.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +24 -0
  4. data/lib/better_auth/adapters/internal_adapter.rb +10 -7
  5. data/lib/better_auth/adapters/memory.rb +57 -11
  6. data/lib/better_auth/adapters/sql.rb +123 -20
  7. data/lib/better_auth/api.rb +114 -9
  8. data/lib/better_auth/async.rb +70 -0
  9. data/lib/better_auth/configuration.rb +97 -7
  10. data/lib/better_auth/context.rb +165 -12
  11. data/lib/better_auth/cookies.rb +6 -4
  12. data/lib/better_auth/core.rb +2 -0
  13. data/lib/better_auth/crypto/jwe.rb +27 -5
  14. data/lib/better_auth/crypto.rb +32 -0
  15. data/lib/better_auth/database_hooks.rb +8 -8
  16. data/lib/better_auth/deprecate.rb +28 -0
  17. data/lib/better_auth/endpoint.rb +92 -5
  18. data/lib/better_auth/error.rb +8 -1
  19. data/lib/better_auth/host.rb +166 -0
  20. data/lib/better_auth/instrumentation.rb +74 -0
  21. data/lib/better_auth/logger.rb +31 -0
  22. data/lib/better_auth/middleware/origin_check.rb +2 -2
  23. data/lib/better_auth/oauth2.rb +94 -0
  24. data/lib/better_auth/plugins/admin/schema.rb +2 -2
  25. data/lib/better_auth/plugins/admin.rb +344 -16
  26. data/lib/better_auth/plugins/anonymous.rb +37 -3
  27. data/lib/better_auth/plugins/device_authorization.rb +102 -5
  28. data/lib/better_auth/plugins/dub.rb +148 -0
  29. data/lib/better_auth/plugins/email_otp.rb +261 -19
  30. data/lib/better_auth/plugins/expo.rb +17 -1
  31. data/lib/better_auth/plugins/generic_oauth.rb +67 -35
  32. data/lib/better_auth/plugins/jwt.rb +37 -4
  33. data/lib/better_auth/plugins/last_login_method.rb +2 -2
  34. data/lib/better_auth/plugins/magic_link.rb +66 -3
  35. data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
  36. data/lib/better_auth/plugins/mcp/config.rb +51 -0
  37. data/lib/better_auth/plugins/mcp/consent.rb +31 -0
  38. data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
  39. data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
  40. data/lib/better_auth/plugins/mcp/registration.rb +31 -0
  41. data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
  42. data/lib/better_auth/plugins/mcp/schema.rb +91 -0
  43. data/lib/better_auth/plugins/mcp/token.rb +108 -0
  44. data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
  45. data/lib/better_auth/plugins/mcp.rb +111 -263
  46. data/lib/better_auth/plugins/multi_session.rb +61 -3
  47. data/lib/better_auth/plugins/oauth_protocol.rb +173 -30
  48. data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
  49. data/lib/better_auth/plugins/oidc_provider.rb +118 -14
  50. data/lib/better_auth/plugins/one_tap.rb +7 -2
  51. data/lib/better_auth/plugins/one_time_token.rb +42 -2
  52. data/lib/better_auth/plugins/open_api.rb +163 -318
  53. data/lib/better_auth/plugins/organization/schema.rb +6 -0
  54. data/lib/better_auth/plugins/organization.rb +186 -56
  55. data/lib/better_auth/plugins/phone_number.rb +141 -6
  56. data/lib/better_auth/plugins/siwe.rb +69 -3
  57. data/lib/better_auth/plugins/two_factor.rb +118 -41
  58. data/lib/better_auth/plugins/username.rb +57 -2
  59. data/lib/better_auth/rate_limiter.rb +38 -0
  60. data/lib/better_auth/request_state.rb +44 -0
  61. data/lib/better_auth/response.rb +42 -0
  62. data/lib/better_auth/router.rb +7 -1
  63. data/lib/better_auth/routes/account.rb +220 -42
  64. data/lib/better_auth/routes/email_verification.rb +98 -14
  65. data/lib/better_auth/routes/password.rb +126 -8
  66. data/lib/better_auth/routes/session.rb +128 -13
  67. data/lib/better_auth/routes/sign_in.rb +26 -2
  68. data/lib/better_auth/routes/sign_out.rb +13 -1
  69. data/lib/better_auth/routes/sign_up.rb +70 -4
  70. data/lib/better_auth/routes/social.rb +132 -7
  71. data/lib/better_auth/routes/user.rb +228 -20
  72. data/lib/better_auth/routes/validation.rb +50 -0
  73. data/lib/better_auth/secret_config.rb +115 -0
  74. data/lib/better_auth/session.rb +13 -2
  75. data/lib/better_auth/url_helpers.rb +206 -0
  76. data/lib/better_auth/version.rb +1 -1
  77. data/lib/better_auth.rb +12 -0
  78. metadata +23 -1
@@ -6,9 +6,21 @@ module BetterAuth
6
6
  module Plugins
7
7
  module OIDCProvider
8
8
  VALID_PROMPTS = %w[none login consent create select_account].freeze
9
+ DEPRECATION_MESSAGE = 'The "oidc-provider" plugin is deprecated and will be removed in the next major version. Migrate to better_auth-oauth-provider. See: https://www.better-auth.com/docs/plugins/oauth-provider'
9
10
 
10
11
  module_function
11
12
 
13
+ def warn_deprecation!(logger = nil)
14
+ return if @deprecation_warned
15
+
16
+ Deprecate.warn_once("[Deprecation] #{DEPRECATION_MESSAGE}", logger)
17
+ @deprecation_warned = true
18
+ end
19
+
20
+ def reset_deprecation_warning!
21
+ @deprecation_warned = false
22
+ end
23
+
12
24
  def normalize_issuer(value)
13
25
  uri = URI.parse(value.to_s)
14
26
  uri.query = nil
@@ -34,6 +46,9 @@ module BetterAuth
34
46
  module_function
35
47
 
36
48
  def oidc_provider(options = {})
49
+ raw_options = normalize_hash(options)
50
+ OIDCProvider.warn_deprecation!(raw_options[:logger]) unless raw_options[:__skip_deprecation_warning]
51
+
37
52
  config = {
38
53
  code_expires_in: 600,
39
54
  consent_page: "/oauth2/authorize",
@@ -45,7 +60,7 @@ module BetterAuth
45
60
  store_client_secret: "plain",
46
61
  scopes: %w[openid profile email offline_access],
47
62
  store: OAuthProtocol.stores
48
- }.merge(normalize_hash(options))
63
+ }.merge(raw_options.except(:logger, :__skip_deprecation_warning))
49
64
 
50
65
  Plugin.new(
51
66
  id: "oidc-provider",
@@ -113,7 +128,7 @@ module BetterAuth
113
128
  end
114
129
 
115
130
  def oidc_register_endpoint(config)
116
- Endpoint.new(path: "/oauth2/register", method: "POST") do |ctx|
131
+ Endpoint.new(path: "/oauth2/register", method: "POST", metadata: oidc_openapi("registerOAuthApplication", "Register an OAuth2 application", "OAuth2 application registered successfully", oidc_client_schema)) do |ctx|
117
132
  session = Routes.current_session(ctx, allow_nil: true)
118
133
  unless session || config[:allow_dynamic_client_registration]
119
134
  raise APIError.new("UNAUTHORIZED", message: "invalid_token")
@@ -146,7 +161,7 @@ module BetterAuth
146
161
  end
147
162
 
148
163
  def oidc_get_client_endpoint
149
- Endpoint.new(path: "/oauth2/client/:id", method: "GET") do |ctx|
164
+ Endpoint.new(path: "/oauth2/client/:id", method: "GET", metadata: oidc_openapi("getOAuthClient", "Get OAuth2 client details", "OAuth2 client retrieved successfully", oidc_client_schema)) do |ctx|
150
165
  client = OAuthProtocol.find_client(ctx, "oauthApplication", ctx.params["id"] || ctx.params[:id])
151
166
  raise APIError.new("NOT_FOUND", message: "client not found") unless client
152
167
 
@@ -155,7 +170,7 @@ module BetterAuth
155
170
  end
156
171
 
157
172
  def oidc_list_clients_endpoint
158
- Endpoint.new(path: "/oauth2/clients", method: "GET") do |ctx|
173
+ Endpoint.new(path: "/oauth2/clients", method: "GET", metadata: oidc_openapi("listOAuthApplications", "List OAuth2 applications", "OAuth2 applications retrieved successfully", {type: "array", items: oidc_client_schema})) do |ctx|
159
174
  session = Routes.current_session(ctx)
160
175
  clients = ctx.context.adapter.find_many(model: "oauthApplication", where: [{field: "userId", value: session[:user]["id"]}])
161
176
  ctx.json(clients.map { |client| OAuthProtocol.client_response(client, include_secret: false) })
@@ -163,7 +178,7 @@ module BetterAuth
163
178
  end
164
179
 
165
180
  def oidc_update_client_endpoint
166
- Endpoint.new(path: "/oauth2/client/:id", method: "PATCH") do |ctx|
181
+ Endpoint.new(path: "/oauth2/client/:id", method: "PATCH", metadata: oidc_openapi("updateOAuthApplication", "Update an OAuth2 application", "OAuth2 application updated successfully", oidc_client_schema)) do |ctx|
167
182
  session = Routes.current_session(ctx)
168
183
  client = oidc_find_owned_client!(ctx, session)
169
184
  body = OAuthProtocol.stringify_keys(ctx.body)
@@ -191,7 +206,7 @@ module BetterAuth
191
206
  end
192
207
 
193
208
  def oidc_rotate_client_secret_endpoint(config)
194
- Endpoint.new(path: "/oauth2/client/:id/rotate-secret", method: "POST") do |ctx|
209
+ Endpoint.new(path: "/oauth2/client/:id/rotate-secret", method: "POST", metadata: oidc_openapi("rotateOAuthApplicationSecret", "Rotate an OAuth2 application secret", "OAuth2 application secret rotated successfully", oidc_client_schema)) do |ctx|
195
210
  session = Routes.current_session(ctx)
196
211
  client = oidc_find_owned_client!(ctx, session)
197
212
  if OAuthProtocol.stringify_keys(client)["tokenEndpointAuthMethod"] == "none"
@@ -209,7 +224,7 @@ module BetterAuth
209
224
  end
210
225
 
211
226
  def oidc_delete_client_endpoint
212
- Endpoint.new(path: "/oauth2/client/:id", method: "DELETE") do |ctx|
227
+ Endpoint.new(path: "/oauth2/client/:id", method: "DELETE", metadata: oidc_openapi("deleteOAuthApplication", "Delete an OAuth2 application", "OAuth2 application deleted successfully", OpenAPI.success_response_schema)) do |ctx|
213
228
  session = Routes.current_session(ctx)
214
229
  client = oidc_find_owned_client!(ctx, session)
215
230
  ctx.context.adapter.delete(model: "oauthApplication", where: [{field: "id", value: client.fetch("id")}])
@@ -218,7 +233,7 @@ module BetterAuth
218
233
  end
219
234
 
220
235
  def oidc_authorize_endpoint(config)
221
- Endpoint.new(path: "/oauth2/authorize", method: "GET") do |ctx|
236
+ Endpoint.new(path: "/oauth2/authorize", method: "GET", metadata: oidc_openapi("oauth2Authorize", "Authorize an OAuth2 request", "Authorization response generated successfully", {type: "object", additionalProperties: true})) do |ctx|
222
237
  query = OAuthProtocol.stringify_keys(ctx.query)
223
238
  prompts = OIDCProvider.parse_prompt(query["prompt"])
224
239
  session = Routes.current_session(ctx, allow_nil: true)
@@ -308,7 +323,7 @@ module BetterAuth
308
323
  end
309
324
 
310
325
  def oidc_consent_endpoint(config)
311
- Endpoint.new(path: "/oauth2/consent", method: "POST") do |ctx|
326
+ Endpoint.new(path: "/oauth2/consent", method: "POST", metadata: oidc_openapi("oauth2Consent", "Handle OAuth2 consent", "OAuth2 consent handled successfully", oidc_redirect_response_schema)) do |ctx|
312
327
  Routes.current_session(ctx)
313
328
  body = OAuthProtocol.stringify_keys(ctx.body)
314
329
  consent = config[:store][:consents].delete(body["consent_code"].to_s)
@@ -338,7 +353,11 @@ module BetterAuth
338
353
  end
339
354
 
340
355
  def oidc_token_endpoint(config)
341
- Endpoint.new(path: "/oauth2/token", method: "POST", metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}) do |ctx|
356
+ Endpoint.new(
357
+ path: "/oauth2/token",
358
+ method: "POST",
359
+ metadata: oidc_openapi("oauth2Token", "Exchange OAuth2 code for tokens", "OAuth2 tokens issued successfully", oidc_token_response_schema).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
360
+ ) do |ctx|
342
361
  body = OAuthProtocol.stringify_keys(ctx.body)
343
362
  client = OAuthProtocol.authenticate_client!(ctx, "oauthApplication", store_client_secret: config[:store_client_secret])
344
363
  raise APIError.new("UNAUTHORIZED", message: "invalid_client") unless client
@@ -374,13 +393,17 @@ module BetterAuth
374
393
  end
375
394
 
376
395
  def oidc_userinfo_endpoint(config)
377
- Endpoint.new(path: "/oauth2/userinfo", method: "GET") do |ctx|
396
+ Endpoint.new(path: "/oauth2/userinfo", method: "GET", metadata: oidc_openapi("oauth2Userinfo", "Get OAuth2 user information", "User information retrieved successfully", oidc_userinfo_schema)) do |ctx|
378
397
  ctx.json(OAuthProtocol.userinfo(config[:store], ctx.headers["authorization"], additional_claim: config[:get_additional_user_info_claim]))
379
398
  end
380
399
  end
381
400
 
382
401
  def oidc_introspect_endpoint(config)
383
- Endpoint.new(path: "/oauth2/introspect", method: "POST", metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}) do |ctx|
402
+ Endpoint.new(
403
+ path: "/oauth2/introspect",
404
+ method: "POST",
405
+ metadata: oidc_openapi("oauth2Introspect", "Introspect an OAuth2 token", "OAuth2 token introspection result", oidc_introspection_schema).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
406
+ ) do |ctx|
384
407
  OAuthProtocol.authenticate_client!(ctx, "oauthApplication", store_client_secret: config[:store_client_secret])
385
408
  body = OAuthProtocol.stringify_keys(ctx.body)
386
409
  token = config[:store][:tokens][body["token"].to_s] || config[:store][:refresh_tokens][body["token"].to_s]
@@ -396,7 +419,11 @@ module BetterAuth
396
419
  end
397
420
 
398
421
  def oidc_revoke_endpoint(config)
399
- Endpoint.new(path: "/oauth2/revoke", method: "POST", metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}) do |ctx|
422
+ Endpoint.new(
423
+ path: "/oauth2/revoke",
424
+ method: "POST",
425
+ metadata: oidc_openapi("oauth2Revoke", "Revoke an OAuth2 token", "OAuth2 token revoked successfully", OpenAPI.object_schema({revoked: {type: "boolean"}}, required: ["revoked"])).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
426
+ ) do |ctx|
400
427
  OAuthProtocol.authenticate_client!(ctx, "oauthApplication", store_client_secret: config[:store_client_secret])
401
428
  body = OAuthProtocol.stringify_keys(ctx.body)
402
429
  if (token = config[:store][:tokens][body["token"].to_s] || config[:store][:refresh_tokens][body["token"].to_s])
@@ -407,7 +434,11 @@ module BetterAuth
407
434
  end
408
435
 
409
436
  def oidc_end_session_endpoint
410
- Endpoint.new(path: "/oauth2/endsession", method: ["GET", "POST"], metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}) do |ctx|
437
+ Endpoint.new(
438
+ path: "/oauth2/endsession",
439
+ method: ["GET", "POST"],
440
+ metadata: oidc_openapi("oauth2EndSession", "RP-Initiated Logout endpoint", "Logout request handled").merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
441
+ ) do |ctx|
411
442
  input_source = (ctx.method == "GET") ? ctx.query : ctx.body
412
443
  input = OAuthProtocol.stringify_keys(input_source)
413
444
  if input["post_logout_redirect_uri"]
@@ -425,6 +456,79 @@ module BetterAuth
425
456
  end
426
457
  end
427
458
 
459
+ def oidc_openapi(operation_id, description, response_description = "Success", response_schema = {type: "object"})
460
+ {
461
+ openapi: {
462
+ operationId: operation_id,
463
+ description: description,
464
+ responses: {
465
+ "200" => OpenAPI.json_response(response_description, response_schema)
466
+ }
467
+ }
468
+ }
469
+ end
470
+
471
+ def oidc_client_schema
472
+ OpenAPI.object_schema(
473
+ {
474
+ clientId: {type: "string"},
475
+ clientSecret: {type: ["string", "null"]},
476
+ name: {type: "string"},
477
+ redirectUris: {type: "array", items: {type: "string"}},
478
+ grantTypes: {type: "array", items: {type: "string"}},
479
+ responseTypes: {type: "array", items: {type: "string"}}
480
+ },
481
+ required: ["clientId", "name"]
482
+ )
483
+ end
484
+
485
+ def oidc_redirect_response_schema
486
+ OpenAPI.object_schema(
487
+ {redirectURI: {type: "string", format: "uri"}},
488
+ required: ["redirectURI"]
489
+ )
490
+ end
491
+
492
+ def oidc_token_response_schema
493
+ OpenAPI.object_schema(
494
+ {
495
+ access_token: {type: "string"},
496
+ token_type: {type: "string"},
497
+ expires_in: {type: "number"},
498
+ refresh_token: {type: ["string", "null"]},
499
+ id_token: {type: ["string", "null"]},
500
+ scope: {type: ["string", "null"]}
501
+ },
502
+ required: ["access_token", "token_type", "expires_in"]
503
+ )
504
+ end
505
+
506
+ def oidc_userinfo_schema
507
+ OpenAPI.object_schema(
508
+ {
509
+ sub: {type: "string"},
510
+ email: {type: ["string", "null"]},
511
+ email_verified: {type: ["boolean", "null"]},
512
+ name: {type: ["string", "null"]},
513
+ picture: {type: ["string", "null"]}
514
+ },
515
+ required: ["sub"]
516
+ )
517
+ end
518
+
519
+ def oidc_introspection_schema
520
+ OpenAPI.object_schema(
521
+ {
522
+ active: {type: "boolean"},
523
+ client_id: {type: ["string", "null"]},
524
+ scope: {type: ["string", "null"]},
525
+ sub: {type: ["string", "null"]},
526
+ exp: {type: ["number", "null"]}
527
+ },
528
+ required: ["active"]
529
+ )
530
+ end
531
+
428
532
  def oidc_provider_schema
429
533
  {
430
534
  oauthApplication: {
@@ -29,8 +29,12 @@ module BetterAuth
29
29
  },
30
30
  metadata: {
31
31
  openapi: {
32
+ operationId: "oneTapCallback",
32
33
  summary: "One tap callback",
33
- description: "Use this endpoint to authenticate with Google One Tap"
34
+ description: "Use this endpoint to authenticate with Google One Tap",
35
+ responses: {
36
+ "200" => OpenAPI.json_response("Success", OpenAPI.session_response_schema_pair)
37
+ }
34
38
  }
35
39
  }
36
40
  ) do |ctx|
@@ -61,7 +65,8 @@ module BetterAuth
61
65
  providerId: "google",
62
66
  accountId: fetch_value(payload, "sub").to_s,
63
67
  idToken: id_token
64
- }
68
+ },
69
+ context: ctx
65
70
  )
66
71
  raise APIError.new("INTERNAL_SERVER_ERROR", message: "Could not create user") unless created
67
72
 
@@ -29,7 +29,27 @@ module BetterAuth
29
29
  end
30
30
 
31
31
  def generate_one_time_token_endpoint(config)
32
- Endpoint.new(path: "/one-time-token/generate", method: "GET") do |ctx|
32
+ Endpoint.new(
33
+ path: "/one-time-token/generate",
34
+ method: "GET",
35
+ metadata: {
36
+ openapi: {
37
+ operationId: "generateOneTimeToken",
38
+ description: "Generate a one-time token for the current session",
39
+ responses: {
40
+ "200" => OpenAPI.json_response(
41
+ "One-time token",
42
+ OpenAPI.object_schema(
43
+ {
44
+ token: {type: "string"}
45
+ },
46
+ required: ["token"]
47
+ )
48
+ )
49
+ }
50
+ }
51
+ }
52
+ ) do |ctx|
33
53
  if config[:disable_client_request] && ctx.request
34
54
  raise APIError.new("BAD_REQUEST", message: "Client requests are disabled")
35
55
  end
@@ -41,7 +61,27 @@ module BetterAuth
41
61
  end
42
62
 
43
63
  def verify_one_time_token_endpoint(config)
44
- Endpoint.new(path: "/one-time-token/verify", method: "POST") do |ctx|
64
+ Endpoint.new(
65
+ path: "/one-time-token/verify",
66
+ method: "POST",
67
+ metadata: {
68
+ openapi: {
69
+ operationId: "verifyOneTimeToken",
70
+ description: "Verify a one-time token and restore its session",
71
+ requestBody: OpenAPI.json_request_body(
72
+ OpenAPI.object_schema(
73
+ {
74
+ token: {type: "string"}
75
+ },
76
+ required: ["token"]
77
+ )
78
+ ),
79
+ responses: {
80
+ "200" => OpenAPI.json_response("Session restored", OpenAPI.session_response_schema_pair)
81
+ }
82
+ }
83
+ }
84
+ ) do |ctx|
45
85
  body = normalize_hash(ctx.body)
46
86
  token = body[:token].to_s
47
87
  stored_token = one_time_token_stored_value(config, token)