better_auth 0.10.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 +83 -0
- data/LICENSE.md +1 -1
- data/README.md +138 -91
- 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 +191 -56
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +294 -49
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +41 -46
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +110 -14
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +14 -3
- data/lib/better_auth/error.rb +3 -1
- 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 +4 -4
- 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 +79 -28
- 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 +47 -9
- data/lib/better_auth/plugins/dub.rb +2 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +141 -146
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +37 -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 +142 -32
- 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 +61 -60
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +35 -7
- data/lib/better_auth/plugins/organization.rb +455 -122
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +24 -26
- 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 +186 -21
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +297 -135
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +40 -8
- data/lib/better_auth/routes/email_verification.rb +88 -5
- data/lib/better_auth/routes/password.rb +5 -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 +169 -54
- data/lib/better_auth/routes/user.rb +11 -8
- data/lib/better_auth/schema/sql.rb +90 -24
- data/lib/better_auth/schema.rb +124 -3
- data/lib/better_auth/session.rb +27 -9
- 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 +41 -14
- 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/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 +5 -5
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +11 -41
- metadata +5 -13
- 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 -256
- data/lib/better_auth/plugins/oidc_provider.rb +0 -769
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
return if defined?(BetterAuth::Plugins::OAUTH_PROVIDER_PLUGIN_IMPLEMENTATION)
|
|
4
|
+
|
|
3
5
|
module BetterAuth
|
|
4
6
|
module Plugins
|
|
5
7
|
module_function
|
|
6
8
|
|
|
7
|
-
def oauth_provider(*args)
|
|
8
|
-
|
|
9
|
-
BetterAuth::Plugins.oauth_provider(*args)
|
|
10
|
-
rescue LoadError => error
|
|
11
|
-
raise if error.path && error.path != "better_auth/oauth_provider"
|
|
12
|
-
|
|
13
|
-
raise LoadError, "BetterAuth::Plugins.oauth_provider requires the better_auth-oauth-provider gem. Add `gem \"better_auth-oauth-provider\"` and `require \"better_auth/oauth_provider\"`."
|
|
9
|
+
def oauth_provider(*args, &block)
|
|
10
|
+
call_external_plugin!(:oauth_provider, *args, implementation_constant: :OAUTH_PROVIDER_PLUGIN_IMPLEMENTATION, gem_name: "better_auth-oauth-provider", entry: "lib/better_auth/oauth_provider.rb", &block)
|
|
14
11
|
end
|
|
15
12
|
end
|
|
16
13
|
end
|
|
@@ -50,36 +50,32 @@ module BetterAuth
|
|
|
50
50
|
id_token = body[:id_token].to_s
|
|
51
51
|
payload = one_tap_verify_id_token(ctx, config, id_token)
|
|
52
52
|
email = fetch_value(payload, "email").to_s.downcase
|
|
53
|
+
raise APIError.new("BAD_REQUEST", message: "invalid id token") if Routes.blank_remote_id?(fetch_value(payload, "sub"))
|
|
53
54
|
|
|
54
55
|
if email.empty?
|
|
55
56
|
next ctx.json({error: "Email not available in token"})
|
|
56
57
|
end
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
raise APIError.new("INTERNAL_SERVER_ERROR", message: "Could not create user") unless created
|
|
80
|
-
|
|
81
|
-
session_data = one_tap_create_session(ctx, created[:user])
|
|
82
|
-
end
|
|
59
|
+
sub = fetch_value(payload, "sub").to_s
|
|
60
|
+
session_data = Routes.persist_social_user(
|
|
61
|
+
ctx,
|
|
62
|
+
"google",
|
|
63
|
+
{
|
|
64
|
+
id: sub,
|
|
65
|
+
email: email,
|
|
66
|
+
emailVerified: one_tap_boolean_value(fetch_value(payload, "email_verified")),
|
|
67
|
+
name: fetch_value(payload, "name").to_s,
|
|
68
|
+
image: fetch_value(payload, "picture")
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
providerId: "google",
|
|
72
|
+
accountId: sub,
|
|
73
|
+
idToken: id_token,
|
|
74
|
+
scope: "openid,profile,email"
|
|
75
|
+
},
|
|
76
|
+
disable_sign_up: config[:disable_signup]
|
|
77
|
+
)
|
|
78
|
+
one_tap_raise_persistence_error!(session_data[:error]) if session_data[:error]
|
|
83
79
|
|
|
84
80
|
Cookies.set_session_cookie(ctx, session_data)
|
|
85
81
|
ctx.json({
|
|
@@ -90,16 +86,35 @@ module BetterAuth
|
|
|
90
86
|
end
|
|
91
87
|
|
|
92
88
|
def one_tap_verify_id_token(ctx, config, id_token)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
google_provider = ctx.context.options.social_providers[:google] || {}
|
|
90
|
+
google_options = fetch_value(google_provider, "options") || {}
|
|
91
|
+
audience = config[:client_id]
|
|
92
|
+
audience = fetch_value(google_provider, "client_id") if one_tap_blank_audience?(audience)
|
|
93
|
+
if one_tap_blank_audience?(audience)
|
|
94
|
+
raise APIError.new(
|
|
95
|
+
"BAD_REQUEST",
|
|
96
|
+
message: "Google client ID is required for One Tap. Set it on the one_tap plugin (client_id) or on social_providers.google."
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
verifier = config[:verify_id_token]
|
|
102
|
+
payload = if verifier.respond_to?(:call)
|
|
103
|
+
verifier.call(id_token, ctx, audience: audience)
|
|
104
|
+
else
|
|
105
|
+
one_tap_verify_google_id_token(id_token, audience)
|
|
106
|
+
end
|
|
107
|
+
payload = one_tap_stringify_payload(payload)
|
|
108
|
+
hosted_domain = fetch_value(google_provider, "hd")
|
|
109
|
+
hosted_domain = fetch_value(google_options, "hd") if hosted_domain.nil?
|
|
110
|
+
unless SocialProviders.google_hosted_domain_allowed?(hosted_domain, payload["hd"])
|
|
111
|
+
raise "Invalid Google hosted domain"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
payload
|
|
115
|
+
rescue
|
|
116
|
+
raise APIError.new("BAD_REQUEST", message: "invalid id token")
|
|
99
117
|
end
|
|
100
|
-
one_tap_stringify_payload(payload)
|
|
101
|
-
rescue
|
|
102
|
-
raise APIError.new("BAD_REQUEST", message: "invalid id token")
|
|
103
118
|
end
|
|
104
119
|
|
|
105
120
|
def one_tap_verify_google_id_token(id_token, audience)
|
|
@@ -109,10 +124,8 @@ module BetterAuth
|
|
|
109
124
|
iss: ["https://accounts.google.com", "accounts.google.com"],
|
|
110
125
|
verify_iss: true
|
|
111
126
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
options[:verify_aud] = true
|
|
115
|
-
end
|
|
127
|
+
options[:aud] = audience
|
|
128
|
+
options[:verify_aud] = true
|
|
116
129
|
payload, = ::JWT.decode(id_token, nil, true, options.merge(jwks: jwks))
|
|
117
130
|
payload
|
|
118
131
|
end
|
|
@@ -129,31 +142,19 @@ module BetterAuth
|
|
|
129
142
|
jwks
|
|
130
143
|
end
|
|
131
144
|
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return if account
|
|
145
|
+
def one_tap_blank_audience?(audience)
|
|
146
|
+
Array(audience).empty? || Array(audience).all? { |value| value.to_s.strip.empty? }
|
|
147
|
+
end
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
def one_tap_raise_persistence_error!(error)
|
|
150
|
+
case error
|
|
151
|
+
when "signup disabled"
|
|
152
|
+
raise APIError.new("BAD_GATEWAY", message: "User not found")
|
|
153
|
+
when "account not linked", "banned"
|
|
142
154
|
raise APIError.new("UNAUTHORIZED", message: "Google sub doesn't match")
|
|
155
|
+
else
|
|
156
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: "Could not create user")
|
|
143
157
|
end
|
|
144
|
-
|
|
145
|
-
ctx.context.internal_adapter.link_account(
|
|
146
|
-
userId: user[:user]["id"],
|
|
147
|
-
providerId: "google",
|
|
148
|
-
accountId: sub,
|
|
149
|
-
scope: "openid,profile,email",
|
|
150
|
-
idToken: id_token
|
|
151
|
-
)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def one_tap_create_session(ctx, user)
|
|
155
|
-
session = ctx.context.internal_adapter.create_session(user["id"])
|
|
156
|
-
{session: session, user: user}
|
|
157
158
|
end
|
|
158
159
|
|
|
159
160
|
def one_tap_stringify_payload(payload)
|
|
@@ -85,12 +85,9 @@ module BetterAuth
|
|
|
85
85
|
body = normalize_hash(ctx.body)
|
|
86
86
|
token = body[:token].to_s
|
|
87
87
|
stored_token = one_time_token_stored_value(config, token)
|
|
88
|
-
verification = ctx.context.internal_adapter.
|
|
88
|
+
verification = ctx.context.internal_adapter.consume_verification_value("one-time-token:#{stored_token}")
|
|
89
89
|
raise APIError.new("BAD_REQUEST", message: "Invalid token") unless verification
|
|
90
90
|
|
|
91
|
-
ctx.context.internal_adapter.delete_verification_value(verification["id"])
|
|
92
|
-
raise APIError.new("BAD_REQUEST", message: "Token expired") if Routes.expired_time?(verification["expiresAt"])
|
|
93
|
-
|
|
94
91
|
session = ctx.context.internal_adapter.find_session(verification["value"])
|
|
95
92
|
raise APIError.new("BAD_REQUEST", message: "Session not found") unless session
|
|
96
93
|
raise APIError.new("BAD_REQUEST", message: "Session expired") if Routes.expired_time?(session[:session]["expiresAt"])
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module BetterAuth
|
|
4
4
|
module OpenAPI
|
|
5
|
+
# Better Auth action endpoints advertise upstream camelCase JSON field names in
|
|
6
|
+
# OpenAPI and generated inventories. Runtime may still accept Ruby snake_case
|
|
7
|
+
# aliases via normalize_hash and route validation.
|
|
8
|
+
|
|
5
9
|
module_function
|
|
6
10
|
|
|
7
11
|
def object_schema(properties, required: [])
|
|
@@ -234,7 +238,7 @@ module BetterAuth
|
|
|
234
238
|
Plugin.new(
|
|
235
239
|
id: "open-api",
|
|
236
240
|
endpoints: {
|
|
237
|
-
|
|
241
|
+
generate_openapi_schema: Endpoint.new(path: "/open-api/generate-schema", method: "GET") do |ctx|
|
|
238
242
|
ctx.json(open_api_schema(ctx.context))
|
|
239
243
|
end,
|
|
240
244
|
open_api_reference: Endpoint.new(path: config[:path], method: "GET", metadata: {hide: true}) do |ctx|
|
|
@@ -267,7 +271,7 @@ module BetterAuth
|
|
|
267
271
|
],
|
|
268
272
|
servers: [
|
|
269
273
|
{
|
|
270
|
-
url: context.
|
|
274
|
+
url: context.canonical_base_url
|
|
271
275
|
}
|
|
272
276
|
],
|
|
273
277
|
tags: [
|
|
@@ -308,6 +312,7 @@ module BetterAuth
|
|
|
308
312
|
|
|
309
313
|
def open_api_paths(endpoints, options)
|
|
310
314
|
disabled_paths = Array(options.disabled_paths).map(&:to_s)
|
|
315
|
+
used_operation_ids = Set.new
|
|
311
316
|
endpoints.each_with_object({}) do |(key, endpoint, tag), paths|
|
|
312
317
|
next unless endpoint.path
|
|
313
318
|
next if endpoint.metadata[:exclude_from_openapi] || endpoint.metadata[:SERVER_ONLY] || endpoint.metadata[:server_only]
|
|
@@ -318,7 +323,7 @@ module BetterAuth
|
|
|
318
323
|
path = open_api_path(endpoint.path)
|
|
319
324
|
paths[path] ||= {}
|
|
320
325
|
endpoint.methods.reject { |method| method == "*" }.each do |method|
|
|
321
|
-
paths[path][method.downcase.to_sym] = open_api_operation(endpoint, method, tag)
|
|
326
|
+
paths[path][method.downcase.to_sym] = open_api_operation(endpoint, method, tag, used_operation_ids)
|
|
322
327
|
end
|
|
323
328
|
end
|
|
324
329
|
end
|
|
@@ -331,12 +336,12 @@ module BetterAuth
|
|
|
331
336
|
tag == "Default" && [:ok, :error].include?(key) && endpoint.metadata[:openapi]
|
|
332
337
|
end
|
|
333
338
|
|
|
334
|
-
def open_api_operation(endpoint, method, tag)
|
|
339
|
+
def open_api_operation(endpoint, method, tag, used_operation_ids)
|
|
335
340
|
metadata = endpoint.metadata[:openapi] || {}
|
|
336
341
|
operation = {
|
|
337
342
|
tags: Array(metadata[:tags] || [tag]),
|
|
338
343
|
description: metadata[:description],
|
|
339
|
-
operationId: metadata[:operationId],
|
|
344
|
+
operationId: open_api_operation_id(metadata[:operationId], method, used_operation_ids),
|
|
340
345
|
security: [
|
|
341
346
|
{
|
|
342
347
|
bearerAuth: []
|
|
@@ -353,6 +358,26 @@ module BetterAuth
|
|
|
353
358
|
operation
|
|
354
359
|
end
|
|
355
360
|
|
|
361
|
+
def open_api_operation_id(base_id, method, used_operation_ids)
|
|
362
|
+
return if base_id.to_s.empty?
|
|
363
|
+
|
|
364
|
+
operation_id = base_id.to_s
|
|
365
|
+
unless used_operation_ids.include?(operation_id)
|
|
366
|
+
used_operation_ids << operation_id
|
|
367
|
+
return operation_id
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
suffix = method.to_s.downcase.capitalize
|
|
371
|
+
candidate = "#{operation_id}#{suffix}"
|
|
372
|
+
duplicate_index = 2
|
|
373
|
+
while used_operation_ids.include?(candidate)
|
|
374
|
+
candidate = "#{operation_id}#{suffix}#{duplicate_index}"
|
|
375
|
+
duplicate_index += 1
|
|
376
|
+
end
|
|
377
|
+
used_operation_ids << candidate
|
|
378
|
+
candidate
|
|
379
|
+
end
|
|
380
|
+
|
|
356
381
|
def open_api_components(options)
|
|
357
382
|
Schema.auth_tables(options).each_with_object({}) do |(model, table), schemas|
|
|
358
383
|
name = model.to_s.split(/[_-]/).map(&:capitalize).join
|
|
@@ -361,11 +386,14 @@ module BetterAuth
|
|
|
361
386
|
end
|
|
362
387
|
|
|
363
388
|
def schema_for_table(table)
|
|
364
|
-
required = []
|
|
389
|
+
required = ["id"]
|
|
365
390
|
properties = table[:fields].each_with_object({}) do |(field, attributes), result|
|
|
391
|
+
next if field.to_s == "id"
|
|
392
|
+
|
|
366
393
|
result[field.to_sym] = field_schema(attributes)
|
|
367
|
-
required << field if attributes[:required] && attributes[:
|
|
394
|
+
required << field if attributes[:required] && attributes[:returned] != false
|
|
368
395
|
end
|
|
396
|
+
properties = {id: {type: "string", readOnly: true}}.merge(properties)
|
|
369
397
|
{type: "object", properties: properties, required: required}
|
|
370
398
|
end
|
|
371
399
|
|