better_auth 0.4.0 → 0.6.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
@@ -36,7 +36,25 @@ module BetterAuth
36
36
  end
37
37
 
38
38
  def list_device_sessions_endpoint
39
- Endpoint.new(path: "/multi-session/list-device-sessions", method: "GET") do |ctx|
39
+ Endpoint.new(
40
+ path: "/multi-session/list-device-sessions",
41
+ method: "GET",
42
+ metadata: {
43
+ openapi: {
44
+ operationId: "listDeviceSessions",
45
+ description: "List device sessions",
46
+ responses: {
47
+ "200" => OpenAPI.json_response(
48
+ "Device sessions",
49
+ {
50
+ type: "array",
51
+ items: OpenAPI.session_response_schema_pair
52
+ }
53
+ )
54
+ }
55
+ }
56
+ }
57
+ ) do |ctx|
40
58
  tokens = verified_multi_session_tokens(ctx)
41
59
  sessions = ctx.context.internal_adapter.find_sessions(tokens)
42
60
  .reject { |entry| entry[:session]["expiresAt"] && entry[:session]["expiresAt"] <= Time.now }
@@ -47,7 +65,27 @@ module BetterAuth
47
65
  end
48
66
 
49
67
  def set_active_session_endpoint
50
- Endpoint.new(path: "/multi-session/set-active", method: "POST") do |ctx|
68
+ Endpoint.new(
69
+ path: "/multi-session/set-active",
70
+ method: "POST",
71
+ metadata: {
72
+ openapi: {
73
+ operationId: "setActiveSession",
74
+ description: "Set the active session",
75
+ requestBody: OpenAPI.json_request_body(
76
+ OpenAPI.object_schema(
77
+ {
78
+ sessionToken: {type: "string", description: "The session token"}
79
+ },
80
+ required: ["sessionToken"]
81
+ )
82
+ ),
83
+ responses: {
84
+ "200" => OpenAPI.json_response("Active session", OpenAPI.session_response_schema_pair)
85
+ }
86
+ }
87
+ }
88
+ ) do |ctx|
51
89
  token = fetch_value(ctx.body, "sessionToken").to_s
52
90
  cookie_name = multi_session_cookie_name(ctx, token)
53
91
  unless !token.empty? && ctx.get_signed_cookie(cookie_name, ctx.context.secret)
@@ -66,7 +104,27 @@ module BetterAuth
66
104
  end
67
105
 
68
106
  def revoke_device_session_endpoint
69
- Endpoint.new(path: "/multi-session/revoke", method: "POST") do |ctx|
107
+ Endpoint.new(
108
+ path: "/multi-session/revoke",
109
+ method: "POST",
110
+ metadata: {
111
+ openapi: {
112
+ operationId: "revokeDeviceSession",
113
+ description: "Revoke a device session",
114
+ requestBody: OpenAPI.json_request_body(
115
+ OpenAPI.object_schema(
116
+ {
117
+ sessionToken: {type: "string", description: "The session token"}
118
+ },
119
+ required: ["sessionToken"]
120
+ )
121
+ ),
122
+ responses: {
123
+ "200" => OpenAPI.json_response("Device session revoked", OpenAPI.status_response_schema)
124
+ }
125
+ }
126
+ }
127
+ ) do |ctx|
70
128
  current = Routes.current_session(ctx)
71
129
  token = fetch_value(ctx.body, "sessionToken").to_s
72
130
  cookie_name = multi_session_cookie_name(ctx, token)
@@ -788,7 +788,7 @@ module BetterAuth
788
788
  def store_client_secret_value(ctx, secret, mode)
789
789
  mode = normalize_secret_storage_mode(mode)
790
790
  return Crypto.sha256(secret, encoding: :base64url) if mode == "hashed"
791
- return Crypto.symmetric_encrypt(key: ctx.context.secret, data: secret) if mode == "encrypted"
791
+ return Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: secret) if mode == "encrypted"
792
792
 
793
793
  if mode.is_a?(Hash)
794
794
  return mode[:hash].call(secret) if mode[:hash].respond_to?(:call)
@@ -801,7 +801,7 @@ module BetterAuth
801
801
  def verify_client_secret(ctx, stored_secret, provided_secret, mode)
802
802
  mode = normalize_secret_storage_mode(mode)
803
803
  return Crypto.constant_time_compare(Crypto.sha256(provided_secret, encoding: :base64url), stored_secret.to_s) if mode == "hashed"
804
- return Crypto.symmetric_decrypt(key: ctx.context.secret, data: stored_secret) == provided_secret.to_s if mode == "encrypted"
804
+ return Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_secret) == provided_secret.to_s if mode == "encrypted"
805
805
 
806
806
  if mode.is_a?(Hash)
807
807
  return mode[:hash].call(provided_secret).to_s == stored_secret.to_s if mode[:hash].respond_to?(:call)
@@ -43,12 +43,28 @@ module BetterAuth
43
43
  end
44
44
 
45
45
  def oauth_proxy_endpoint(config)
46
- Endpoint.new(path: "/oauth-proxy-callback", method: "GET") do |ctx|
46
+ Endpoint.new(
47
+ path: "/oauth-proxy-callback",
48
+ method: "GET",
49
+ metadata: {
50
+ openapi: {
51
+ operationId: "oauthProxyCallback",
52
+ description: "OAuth Proxy Callback",
53
+ parameters: [
54
+ {in: "query", name: "callbackURL", required: true, schema: {type: "string", format: "uri"}},
55
+ {in: "query", name: "cookies", required: true, schema: {type: "string"}}
56
+ ],
57
+ responses: {
58
+ "302" => {description: "Redirects to the callback URL"}
59
+ }
60
+ }
61
+ }
62
+ ) do |ctx|
47
63
  query = normalize_hash(ctx.query)
48
64
  callback_url = query[:callback_url] || "/"
49
65
  oauth_proxy_validate_callback!(ctx, callback_url)
50
66
 
51
- decrypted = Crypto.symmetric_decrypt(key: ctx.context.secret, data: query[:cookies].to_s)
67
+ decrypted = Crypto.symmetric_decrypt(key: oauth_proxy_secret(ctx, config), data: query[:cookies].to_s)
52
68
  raise ctx.redirect(oauth_proxy_error_url(ctx, "OAuthProxy - Invalid cookies or secret")) unless decrypted
53
69
 
54
70
  payload = JSON.parse(decrypted)
@@ -83,11 +99,11 @@ module BetterAuth
83
99
  nil
84
100
  end
85
101
 
86
- def oauth_proxy_restore_state_package(ctx, _config)
102
+ def oauth_proxy_restore_state_package(ctx, config)
87
103
  state = fetch_value(ctx.query, "state") || fetch_value(ctx.body, "state")
88
104
  return if state.to_s.empty?
89
105
 
90
- decrypted = Crypto.symmetric_decrypt(key: ctx.context.secret, data: state.to_s)
106
+ decrypted = Crypto.symmetric_decrypt(key: oauth_proxy_secret(ctx, config), data: state.to_s)
91
107
  return unless decrypted
92
108
 
93
109
  package = JSON.parse(decrypted)
@@ -121,7 +137,7 @@ module BetterAuth
121
137
  return if state_cookie.to_s.empty?
122
138
 
123
139
  encrypted_package = Crypto.symmetric_encrypt(
124
- key: ctx.context.secret,
140
+ key: oauth_proxy_secret(ctx, config),
125
141
  data: JSON.generate({
126
142
  state: original_state,
127
143
  stateCookie: state_cookie,
@@ -160,7 +176,7 @@ module BetterAuth
160
176
  return if set_cookie.to_s.empty?
161
177
 
162
178
  encrypted = Crypto.symmetric_encrypt(
163
- key: ctx.context.secret,
179
+ key: oauth_proxy_secret(ctx, config),
164
180
  data: JSON.generate({
165
181
  cookies: set_cookie,
166
182
  timestamp: (Time.now.to_f * 1000).to_i
@@ -180,6 +196,10 @@ module BetterAuth
180
196
  exact && exact[:value]
181
197
  end
182
198
 
199
+ def oauth_proxy_secret(ctx, config)
200
+ config[:secret] || ctx.context.secret_config
201
+ end
202
+
183
203
  def oauth_proxy_sign_in_path?(path)
184
204
  path.to_s.start_with?("/sign-in/social", "/sign-in/oauth2")
185
205
  end
@@ -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)