better_auth 0.4.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/README.md +24 -0
  4. data/lib/better_auth/adapters/internal_adapter.rb +5 -5
  5. data/lib/better_auth/adapters/sql.rb +96 -18
  6. data/lib/better_auth/api.rb +113 -13
  7. data/lib/better_auth/configuration.rb +97 -7
  8. data/lib/better_auth/context.rb +165 -12
  9. data/lib/better_auth/cookies.rb +6 -4
  10. data/lib/better_auth/core.rb +2 -0
  11. data/lib/better_auth/crypto/jwe.rb +27 -5
  12. data/lib/better_auth/crypto.rb +32 -0
  13. data/lib/better_auth/database_hooks.rb +5 -5
  14. data/lib/better_auth/endpoint.rb +87 -3
  15. data/lib/better_auth/error.rb +8 -1
  16. data/lib/better_auth/plugins/admin/schema.rb +2 -2
  17. data/lib/better_auth/plugins/admin.rb +344 -16
  18. data/lib/better_auth/plugins/anonymous.rb +37 -3
  19. data/lib/better_auth/plugins/device_authorization.rb +102 -5
  20. data/lib/better_auth/plugins/dub.rb +148 -0
  21. data/lib/better_auth/plugins/email_otp.rb +246 -15
  22. data/lib/better_auth/plugins/expo.rb +17 -1
  23. data/lib/better_auth/plugins/generic_oauth.rb +53 -7
  24. data/lib/better_auth/plugins/jwt.rb +37 -4
  25. data/lib/better_auth/plugins/last_login_method.rb +2 -2
  26. data/lib/better_auth/plugins/magic_link.rb +66 -3
  27. data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
  28. data/lib/better_auth/plugins/mcp/config.rb +51 -0
  29. data/lib/better_auth/plugins/mcp/consent.rb +31 -0
  30. data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
  31. data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
  32. data/lib/better_auth/plugins/mcp/registration.rb +31 -0
  33. data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
  34. data/lib/better_auth/plugins/mcp/schema.rb +91 -0
  35. data/lib/better_auth/plugins/mcp/token.rb +108 -0
  36. data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
  37. data/lib/better_auth/plugins/mcp.rb +111 -263
  38. data/lib/better_auth/plugins/multi_session.rb +61 -3
  39. data/lib/better_auth/plugins/oauth_protocol.rb +2 -2
  40. data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
  41. data/lib/better_auth/plugins/oidc_provider.rb +118 -14
  42. data/lib/better_auth/plugins/one_tap.rb +7 -2
  43. data/lib/better_auth/plugins/one_time_token.rb +42 -2
  44. data/lib/better_auth/plugins/open_api.rb +163 -318
  45. data/lib/better_auth/plugins/organization.rb +135 -36
  46. data/lib/better_auth/plugins/phone_number.rb +141 -6
  47. data/lib/better_auth/plugins/siwe.rb +69 -3
  48. data/lib/better_auth/plugins/two_factor.rb +65 -23
  49. data/lib/better_auth/plugins/username.rb +57 -2
  50. data/lib/better_auth/rate_limiter.rb +20 -0
  51. data/lib/better_auth/response.rb +42 -0
  52. data/lib/better_auth/router.rb +7 -1
  53. data/lib/better_auth/routes/account.rb +204 -38
  54. data/lib/better_auth/routes/email_verification.rb +98 -14
  55. data/lib/better_auth/routes/password.rb +125 -8
  56. data/lib/better_auth/routes/session.rb +128 -13
  57. data/lib/better_auth/routes/sign_in.rb +24 -2
  58. data/lib/better_auth/routes/sign_out.rb +13 -1
  59. data/lib/better_auth/routes/sign_up.rb +62 -4
  60. data/lib/better_auth/routes/social.rb +102 -7
  61. data/lib/better_auth/routes/user.rb +222 -20
  62. data/lib/better_auth/routes/validation.rb +50 -0
  63. data/lib/better_auth/secret_config.rb +115 -0
  64. data/lib/better_auth/session.rb +1 -1
  65. data/lib/better_auth/url_helpers.rb +12 -1
  66. data/lib/better_auth/version.rb +1 -1
  67. data/lib/better_auth.rb +4 -0
  68. metadata +15 -1
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Plugins
5
+ module_function
6
+
7
+ def dub(options = {})
8
+ config = normalize_hash(options)
9
+ oauth_plugin = dub_oauth_plugin(config[:oauth])
10
+ endpoints = {dub_link: dub_link_endpoint(oauth_plugin)}
11
+ endpoints[:dub_o_auth2_callback] = oauth_plugin.endpoints.fetch(:o_auth2_callback) if oauth_plugin
12
+
13
+ Plugin.new(
14
+ id: "dub",
15
+ endpoints: endpoints,
16
+ init: ->(_context) {
17
+ {
18
+ options: {
19
+ database_hooks: {
20
+ user: {
21
+ create: {
22
+ after: ->(user, ctx) { dub_track_lead(config, user, ctx) }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ },
29
+ options: config
30
+ )
31
+ end
32
+
33
+ def dub_link_endpoint(oauth_plugin)
34
+ Endpoint.new(
35
+ path: "/dub/link",
36
+ method: "POST",
37
+ metadata: {
38
+ openapi: {
39
+ operationId: "dubLink",
40
+ description: "Link a Dub OAuth account",
41
+ responses: {
42
+ "200" => OpenAPI.json_response(
43
+ "Authorization URL generated successfully for linking a Dub account",
44
+ OpenAPI.object_schema(
45
+ {
46
+ url: {type: "string"},
47
+ redirect: {type: "boolean"}
48
+ },
49
+ required: ["url", "redirect"]
50
+ )
51
+ )
52
+ }
53
+ }
54
+ }
55
+ ) do |ctx|
56
+ unless oauth_plugin
57
+ raise APIError.new("NOT_FOUND", message: "Dub OAuth is not configured")
58
+ end
59
+
60
+ body = normalize_hash(ctx.body)
61
+ callback_url = body[:callback_url] || body[:callbackURL]
62
+ if callback_url.to_s.empty?
63
+ raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VALIDATION_ERROR"])
64
+ end
65
+ Routes.validate_auth_callback_url!(ctx.context, callback_url, "callbackURL")
66
+
67
+ ctx.body = body.merge(provider_id: "dub", callback_url: callback_url)
68
+ oauth_plugin.endpoints.fetch(:o_auth2_link_account).call(ctx)
69
+ end
70
+ end
71
+
72
+ def dub_oauth_plugin(oauth_options)
73
+ oauth = normalize_hash(oauth_options || {})
74
+ return nil if oauth.empty?
75
+
76
+ generic_oauth(
77
+ config: [
78
+ {
79
+ provider_id: "dub",
80
+ authorization_url: "https://app.dub.co/oauth/authorize",
81
+ token_url: "https://api.dub.co/oauth/token",
82
+ client_id: oauth[:client_id],
83
+ client_secret: oauth[:client_secret],
84
+ pkce: oauth.key?(:pkce) ? oauth[:pkce] : true
85
+ }
86
+ ]
87
+ )
88
+ end
89
+
90
+ def dub_track_lead(config, user, ctx)
91
+ return unless ctx
92
+
93
+ dub_id = ctx.get_cookie("dub_id")
94
+ return if dub_id.to_s.empty?
95
+ return if config[:disable_lead_tracking]
96
+
97
+ custom = config[:custom_lead_track]
98
+ if custom.respond_to?(:call)
99
+ custom.call(user, ctx)
100
+ else
101
+ dub_default_lead_track(config, user, dub_id, ctx)
102
+ end
103
+
104
+ ctx.set_cookie("dub_id", "", expires: Time.at(0), max_age: 0)
105
+ end
106
+
107
+ def dub_default_lead_track(config, user, dub_id, ctx)
108
+ track = config[:dub_client]&.track
109
+ return unless track&.respond_to?(:lead)
110
+
111
+ dub_invoke_lead(
112
+ track,
113
+ click_id: dub_id,
114
+ event_name: config[:lead_event_name] || "Sign Up",
115
+ customer_external_id: fetch_value(user, "id"),
116
+ customer_name: fetch_value(user, "name"),
117
+ customer_email: fetch_value(user, "email"),
118
+ customer_avatar: fetch_value(user, "image")
119
+ )
120
+ rescue => error
121
+ dub_log_error(ctx, error)
122
+ end
123
+
124
+ def dub_log_error(ctx, error)
125
+ logger = ctx.context.logger
126
+ if logger.respond_to?(:error)
127
+ logger.error(error)
128
+ elsif logger.respond_to?(:call)
129
+ logger.call(:error, error)
130
+ end
131
+ end
132
+
133
+ def dub_invoke_lead(track, payload)
134
+ if track.method(:lead).parameters.any? { |type, name| [:key, :keyreq].include?(type) && name == :request }
135
+ track.lead(request: dub_lead_request_body(payload))
136
+ else
137
+ track.lead(payload)
138
+ end
139
+ end
140
+
141
+ def dub_lead_request_body(payload)
142
+ klass = defined?(::OpenApiSDK::Models::Operations::TrackLeadRequestBody) && ::OpenApiSDK::Models::Operations::TrackLeadRequestBody
143
+ return payload unless klass
144
+
145
+ klass.new(**payload)
146
+ end
147
+ end
148
+ end
@@ -77,7 +77,28 @@ module BetterAuth
77
77
  end
78
78
 
79
79
  def send_verification_otp_endpoint(config)
80
- Endpoint.new(path: "/email-otp/send-verification-otp", method: "POST") do |ctx|
80
+ Endpoint.new(
81
+ path: "/email-otp/send-verification-otp",
82
+ method: "POST",
83
+ metadata: {
84
+ openapi: {
85
+ operationId: "sendVerificationOTP",
86
+ description: "Send an email verification OTP",
87
+ requestBody: OpenAPI.json_request_body(
88
+ OpenAPI.object_schema(
89
+ {
90
+ email: {type: "string"},
91
+ type: {type: "string", enum: ["email-verification", "sign-in", "forget-password"]}
92
+ },
93
+ required: ["email", "type"]
94
+ )
95
+ ),
96
+ responses: {
97
+ "200" => OpenAPI.json_response("OTP sent", OpenAPI.success_response_schema)
98
+ }
99
+ }
100
+ }
101
+ ) do |ctx|
81
102
  body = normalize_hash(ctx.body)
82
103
  email = body[:email].to_s.downcase
83
104
  type = body[:type].to_s
@@ -111,7 +132,30 @@ module BetterAuth
111
132
  end
112
133
 
113
134
  def get_verification_otp_endpoint(config)
114
- Endpoint.new(path: "/email-otp/get-verification-otp", method: "GET") do |ctx|
135
+ Endpoint.new(
136
+ path: "/email-otp/get-verification-otp",
137
+ method: "GET",
138
+ metadata: {
139
+ openapi: {
140
+ operationId: "getVerificationOTP",
141
+ description: "Get a stored verification OTP when storage allows plaintext access",
142
+ parameters: [
143
+ {name: "email", in: "query", required: true, schema: {type: "string"}},
144
+ {name: "type", in: "query", required: true, schema: {type: "string"}}
145
+ ],
146
+ responses: {
147
+ "200" => OpenAPI.json_response(
148
+ "Stored OTP",
149
+ OpenAPI.object_schema(
150
+ {
151
+ otp: {type: ["string", "null"]}
152
+ }
153
+ )
154
+ )
155
+ }
156
+ }
157
+ }
158
+ ) do |ctx|
115
159
  query = normalize_hash(ctx.query)
116
160
  email = query[:email].to_s.downcase
117
161
  type = query[:type].to_s
@@ -124,7 +168,7 @@ module BetterAuth
124
168
  when "hashed"
125
169
  raise APIError.new("BAD_REQUEST", message: "OTP is hashed, cannot return the plain text OTP")
126
170
  when "encrypted"
127
- next ctx.json({otp: Crypto.symmetric_decrypt(key: ctx.context.secret, data: stored_otp)})
171
+ next ctx.json({otp: Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_otp)})
128
172
  end
129
173
 
130
174
  storage = config[:store_otp]
@@ -139,7 +183,29 @@ module BetterAuth
139
183
  end
140
184
 
141
185
  def check_verification_otp_endpoint(config)
142
- Endpoint.new(path: "/email-otp/check-verification-otp", method: "POST") do |ctx|
186
+ Endpoint.new(
187
+ path: "/email-otp/check-verification-otp",
188
+ method: "POST",
189
+ metadata: {
190
+ openapi: {
191
+ operationId: "checkVerificationOTP",
192
+ description: "Check an email verification OTP without consuming it",
193
+ requestBody: OpenAPI.json_request_body(
194
+ OpenAPI.object_schema(
195
+ {
196
+ email: {type: "string"},
197
+ type: {type: "string"},
198
+ otp: {type: "string"}
199
+ },
200
+ required: ["email", "type", "otp"]
201
+ )
202
+ ),
203
+ responses: {
204
+ "200" => OpenAPI.json_response("OTP is valid", OpenAPI.success_response_schema)
205
+ }
206
+ }
207
+ }
208
+ ) do |ctx|
143
209
  body = normalize_hash(ctx.body)
144
210
  email = body[:email].to_s.downcase
145
211
  type = body[:type].to_s
@@ -154,7 +220,38 @@ module BetterAuth
154
220
  end
155
221
 
156
222
  def verify_email_otp_endpoint(config)
157
- Endpoint.new(path: "/email-otp/verify-email", method: "POST") do |ctx|
223
+ Endpoint.new(
224
+ path: "/email-otp/verify-email",
225
+ method: "POST",
226
+ metadata: {
227
+ openapi: {
228
+ operationId: "verifyEmailOTP",
229
+ description: "Verify an email address with an OTP",
230
+ requestBody: OpenAPI.json_request_body(
231
+ OpenAPI.object_schema(
232
+ {
233
+ email: {type: "string"},
234
+ otp: {type: "string"}
235
+ },
236
+ required: ["email", "otp"]
237
+ )
238
+ ),
239
+ responses: {
240
+ "200" => OpenAPI.json_response(
241
+ "Email verified",
242
+ OpenAPI.object_schema(
243
+ {
244
+ status: {type: "boolean"},
245
+ token: {type: ["string", "null"]},
246
+ user: {type: "object", "$ref": "#/components/schemas/User"}
247
+ },
248
+ required: ["status", "user"]
249
+ )
250
+ )
251
+ }
252
+ }
253
+ }
254
+ ) do |ctx|
158
255
  body = normalize_hash(ctx.body)
159
256
  email = body[:email].to_s.downcase
160
257
  otp = body[:otp].to_s
@@ -183,7 +280,37 @@ module BetterAuth
183
280
  end
184
281
 
185
282
  def sign_in_email_otp_endpoint(config)
186
- Endpoint.new(path: "/sign-in/email-otp", method: "POST") do |ctx|
283
+ Endpoint.new(
284
+ path: "/sign-in/email-otp",
285
+ method: "POST",
286
+ metadata: {
287
+ openapi: {
288
+ operationId: "signInEmailOTP",
289
+ description: "Sign in with an email OTP",
290
+ requestBody: OpenAPI.json_request_body(
291
+ OpenAPI.object_schema(
292
+ {
293
+ email: {type: "string"},
294
+ otp: {type: "string"}
295
+ },
296
+ required: ["email", "otp"]
297
+ )
298
+ ),
299
+ responses: {
300
+ "200" => OpenAPI.json_response(
301
+ "Signed in",
302
+ OpenAPI.object_schema(
303
+ {
304
+ token: {type: "string"},
305
+ user: {type: "object", "$ref": "#/components/schemas/User"}
306
+ },
307
+ required: ["token", "user"]
308
+ )
309
+ )
310
+ }
311
+ }
312
+ }
313
+ ) do |ctx|
187
314
  body = normalize_hash(ctx.body)
188
315
  email = body[:email].to_s.downcase
189
316
  otp = body[:otp].to_s
@@ -195,7 +322,7 @@ module BetterAuth
195
322
  else
196
323
  raise APIError.new("BAD_REQUEST", message: EMAIL_OTP_ERROR_CODES["INVALID_OTP"]) if config[:disable_sign_up]
197
324
 
198
- ctx.context.internal_adapter.create_user(email_otp_sign_up_user_data(ctx, body, email))
325
+ ctx.context.internal_adapter.create_user(email_otp_sign_up_user_data(ctx, body, email), context: ctx)
199
326
  end
200
327
 
201
328
  unless user["emailVerified"]
@@ -209,7 +336,28 @@ module BetterAuth
209
336
  end
210
337
 
211
338
  def request_email_change_email_otp_endpoint(config)
212
- Endpoint.new(path: "/email-otp/request-email-change", method: "POST") do |ctx|
339
+ Endpoint.new(
340
+ path: "/email-otp/request-email-change",
341
+ method: "POST",
342
+ metadata: {
343
+ openapi: {
344
+ operationId: "requestEmailChangeOTP",
345
+ description: "Request an OTP to change the current user's email",
346
+ requestBody: OpenAPI.json_request_body(
347
+ OpenAPI.object_schema(
348
+ {
349
+ newEmail: {type: "string"},
350
+ otp: {type: ["string", "null"]}
351
+ },
352
+ required: ["newEmail"]
353
+ )
354
+ ),
355
+ responses: {
356
+ "200" => OpenAPI.json_response("Change email OTP sent", OpenAPI.success_response_schema)
357
+ }
358
+ }
359
+ }
360
+ ) do |ctx|
213
361
  email_otp_change_email_enabled!(config)
214
362
  session = Routes.current_session(ctx)
215
363
  body = normalize_hash(ctx.body)
@@ -235,7 +383,28 @@ module BetterAuth
235
383
  end
236
384
 
237
385
  def change_email_email_otp_endpoint(config)
238
- Endpoint.new(path: "/email-otp/change-email", method: "POST") do |ctx|
386
+ Endpoint.new(
387
+ path: "/email-otp/change-email",
388
+ method: "POST",
389
+ metadata: {
390
+ openapi: {
391
+ operationId: "changeEmailWithEmailOTP",
392
+ description: "Change the current user's email with an OTP",
393
+ requestBody: OpenAPI.json_request_body(
394
+ OpenAPI.object_schema(
395
+ {
396
+ newEmail: {type: "string"},
397
+ otp: {type: "string"}
398
+ },
399
+ required: ["newEmail", "otp"]
400
+ )
401
+ ),
402
+ responses: {
403
+ "200" => OpenAPI.json_response("Email changed", OpenAPI.success_response_schema)
404
+ }
405
+ }
406
+ }
407
+ ) do |ctx|
239
408
  email_otp_change_email_enabled!(config)
240
409
  session = Routes.current_session(ctx)
241
410
  body = normalize_hash(ctx.body)
@@ -259,19 +428,81 @@ module BetterAuth
259
428
  end
260
429
 
261
430
  def request_password_reset_email_otp_endpoint(config)
262
- Endpoint.new(path: "/email-otp/request-password-reset", method: "POST") do |ctx|
431
+ Endpoint.new(
432
+ path: "/email-otp/request-password-reset",
433
+ method: "POST",
434
+ metadata: {
435
+ openapi: {
436
+ operationId: "requestPasswordResetEmailOTP",
437
+ description: "Request a password reset OTP by email",
438
+ requestBody: OpenAPI.json_request_body(
439
+ OpenAPI.object_schema(
440
+ {
441
+ email: {type: "string"}
442
+ },
443
+ required: ["email"]
444
+ )
445
+ ),
446
+ responses: {
447
+ "200" => OpenAPI.json_response("Password reset OTP requested", OpenAPI.status_response_schema)
448
+ }
449
+ }
450
+ }
451
+ ) do |ctx|
263
452
  email_otp_password_reset_request(ctx, config)
264
453
  end
265
454
  end
266
455
 
267
456
  def forget_password_email_otp_endpoint(config)
268
- Endpoint.new(path: "/forget-password/email-otp", method: "POST") do |ctx|
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|
269
478
  email_otp_password_reset_request(ctx, config)
270
479
  end
271
480
  end
272
481
 
273
482
  def reset_password_email_otp_endpoint(config)
274
- Endpoint.new(path: "/email-otp/reset-password", method: "POST") do |ctx|
483
+ Endpoint.new(
484
+ path: "/email-otp/reset-password",
485
+ method: "POST",
486
+ metadata: {
487
+ openapi: {
488
+ operationId: "resetPasswordEmailOTP",
489
+ description: "Reset a password with an email OTP",
490
+ requestBody: OpenAPI.json_request_body(
491
+ OpenAPI.object_schema(
492
+ {
493
+ email: {type: "string"},
494
+ otp: {type: "string"},
495
+ password: {type: "string"}
496
+ },
497
+ required: ["email", "otp", "password"]
498
+ )
499
+ ),
500
+ responses: {
501
+ "200" => OpenAPI.json_response("Password reset", OpenAPI.status_response_schema)
502
+ }
503
+ }
504
+ }
505
+ ) do |ctx|
275
506
  body = normalize_hash(ctx.body)
276
507
  email = body[:email].to_s.downcase
277
508
  otp = body[:otp].to_s
@@ -445,7 +676,7 @@ module BetterAuth
445
676
  def email_otp_stored_value(ctx, config, otp)
446
677
  storage = config[:store_otp]
447
678
  return Crypto.sha256(otp, encoding: :base64url) if storage.to_s == "hashed"
448
- return Crypto.symmetric_encrypt(key: ctx.context.secret, data: otp) if storage.to_s == "encrypted"
679
+ return Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: otp) if storage.to_s == "encrypted"
449
680
 
450
681
  if storage.is_a?(Hash)
451
682
  return storage[:hash].call(otp) if storage[:hash].respond_to?(:call)
@@ -460,7 +691,7 @@ module BetterAuth
460
691
  actual, expected = if storage.to_s == "hashed"
461
692
  [Crypto.sha256(otp, encoding: :base64url), stored_otp]
462
693
  elsif storage.to_s == "encrypted"
463
- [Crypto.symmetric_decrypt(key: ctx.context.secret, data: stored_otp), otp]
694
+ [Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_otp), otp]
464
695
  elsif storage.is_a?(Hash) && storage[:hash].respond_to?(:call)
465
696
  [storage[:hash].call(otp), stored_otp]
466
697
  elsif storage.is_a?(Hash) && storage[:decrypt].respond_to?(:call)
@@ -477,7 +708,7 @@ module BetterAuth
477
708
  def email_otp_plain_value(ctx, config, stored_otp)
478
709
  storage = config[:store_otp]
479
710
  return stored_otp if storage.to_s == "plain" || storage.nil?
480
- return Crypto.symmetric_decrypt(key: ctx.context.secret, data: stored_otp) if storage.to_s == "encrypted"
711
+ return Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_otp) if storage.to_s == "encrypted"
481
712
  return storage[:decrypt].call(stored_otp) if storage.is_a?(Hash) && storage[:decrypt].respond_to?(:call)
482
713
 
483
714
  nil
@@ -29,7 +29,23 @@ module BetterAuth
29
29
  end
30
30
 
31
31
  def expo_authorization_proxy_endpoint
32
- Endpoint.new(path: "/expo-authorization-proxy", method: "GET") do |ctx|
32
+ Endpoint.new(
33
+ path: "/expo-authorization-proxy",
34
+ method: "GET",
35
+ metadata: {
36
+ openapi: {
37
+ operationId: "expoAuthorizationProxy",
38
+ description: "Proxy an Expo authorization redirect",
39
+ parameters: [
40
+ {in: "query", name: "authorizationURL", required: true, schema: {type: "string", format: "uri"}},
41
+ {in: "query", name: "oauthState", required: false, schema: {type: "string"}}
42
+ ],
43
+ responses: {
44
+ "302" => {description: "Redirects to the authorization URL"}
45
+ }
46
+ }
47
+ }
48
+ ) do |ctx|
33
49
  authorization_url = ctx.query[:authorizationURL] || ctx.query["authorizationURL"] || ctx.query[:authorization_url] || ctx.query["authorization_url"]
34
50
  oauth_state = ctx.query[:oauthState] || ctx.query["oauthState"] || ctx.query[:oauth_state] || ctx.query["oauth_state"]
35
51
  raise APIError.new("BAD_REQUEST", message: "Unexpected error") if authorization_url.to_s.empty?
@@ -213,7 +213,19 @@ module BetterAuth
213
213
  end
214
214
 
215
215
  def sign_in_with_oauth2_endpoint(config)
216
- Endpoint.new(path: "/sign-in/oauth2", method: "POST") do |ctx|
216
+ Endpoint.new(
217
+ path: "/sign-in/oauth2",
218
+ method: "POST",
219
+ metadata: {
220
+ openapi: {
221
+ operationId: "signInOAuth2",
222
+ description: "Sign in with OAuth2",
223
+ responses: {
224
+ "200" => OpenAPI.json_response("Sign in with OAuth2", generic_oauth_url_response_schema)
225
+ }
226
+ }
227
+ }
228
+ ) do |ctx|
217
229
  body = normalize_hash(ctx.body)
218
230
  provider_id = body[:provider_id].to_s
219
231
  provider = generic_oauth_provider!(config, provider_id)
@@ -223,7 +235,19 @@ module BetterAuth
223
235
  end
224
236
 
225
237
  def o_auth2_link_account_endpoint(config)
226
- Endpoint.new(path: "/oauth2/link", method: "POST") do |ctx|
238
+ Endpoint.new(
239
+ path: "/oauth2/link",
240
+ method: "POST",
241
+ metadata: {
242
+ openapi: {
243
+ operationId: "linkOAuth2",
244
+ description: "Link an OAuth2 account to the current user session",
245
+ responses: {
246
+ "200" => OpenAPI.json_response("Authorization URL generated successfully for linking an OAuth2 account", generic_oauth_url_response_schema)
247
+ }
248
+ }
249
+ }
250
+ ) do |ctx|
227
251
  session = Routes.current_session(ctx)
228
252
  body = normalize_hash(ctx.body)
229
253
  provider_id = body[:provider_id].to_s
@@ -243,8 +267,20 @@ module BetterAuth
243
267
  def o_auth2_callback_endpoint(config)
244
268
  Endpoint.new(
245
269
  path: "/oauth2/callback/:providerId",
246
- method: ["GET", "POST"],
247
- metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}
270
+ method: "GET",
271
+ metadata: {
272
+ allowed_media_types: ["application/x-www-form-urlencoded", "application/json"],
273
+ openapi: {
274
+ operationId: "oauth2Callback",
275
+ description: "OAuth2 callback",
276
+ responses: {
277
+ "200" => OpenAPI.json_response(
278
+ "OAuth2 callback",
279
+ OpenAPI.object_schema({url: {type: "string"}}, required: ["url"])
280
+ )
281
+ }
282
+ }
283
+ }
248
284
  ) do |ctx|
249
285
  query = normalize_hash(ctx.query)
250
286
  provider_id = (fetch_value(ctx.params, "providerId") || query[:provider_id]).to_s
@@ -306,6 +342,16 @@ module BetterAuth
306
342
  end
307
343
  end
308
344
 
345
+ def generic_oauth_url_response_schema
346
+ OpenAPI.object_schema(
347
+ {
348
+ url: {type: "string"},
349
+ redirect: {type: "boolean"}
350
+ },
351
+ required: ["url", "redirect"]
352
+ )
353
+ end
354
+
309
355
  def generic_oauth_authorization_url(ctx, provider, body, link:)
310
356
  authorization_url = provider[:authorization_url] || generic_oauth_discovery(provider)["authorization_endpoint"]
311
357
  token_url = provider[:token_url] || generic_oauth_discovery(provider)["token_endpoint"]
@@ -368,7 +414,7 @@ module BetterAuth
368
414
  state = Crypto.random_string(32)
369
415
  if strategy.to_s == "cookie"
370
416
  cookie = ctx.context.create_auth_cookie("oauth_state", max_age: 600)
371
- encrypted = Crypto.symmetric_encrypt(key: ctx.context.secret, data: JSON.generate(state_data.merge("state" => state)))
417
+ encrypted = Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: JSON.generate(state_data.merge("state" => state)))
372
418
  ctx.set_cookie(cookie.name, encrypted, cookie.attributes)
373
419
  return state
374
420
  end
@@ -416,7 +462,7 @@ module BetterAuth
416
462
  end
417
463
 
418
464
  begin
419
- decrypted = Crypto.symmetric_decrypt(key: ctx.context.secret, data: encrypted)
465
+ decrypted = Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: encrypted)
420
466
  unless decrypted
421
467
  Cookies.expire_cookie(ctx, cookie)
422
468
  raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "please_restart_the_process"))
@@ -525,7 +571,7 @@ module BetterAuth
525
571
  return token if token.to_s.empty?
526
572
  return token unless ctx.context.options.account[:encrypt_oauth_tokens]
527
573
 
528
- Crypto.symmetric_encrypt(key: ctx.context.secret, data: token)
574
+ Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: token)
529
575
  end
530
576
 
531
577
  def generic_oauth_set_account_cookie(ctx, provider_id, account_id, user_id)