better_auth 0.8.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 +92 -0
- data/LICENSE.md +1 -1
- data/README.md +142 -95
- 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 +303 -54
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +390 -63
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +48 -47
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +119 -15
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +108 -0
- data/lib/better_auth/endpoint.rb +88 -5
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/http_client.rb +46 -0
- 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 +15 -0
- data/lib/better_auth/oauth2.rb +1 -1
- 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 +85 -29
- data/lib/better_auth/plugins/anonymous.rb +2 -0
- 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 +77 -5
- data/lib/better_auth/plugins/dub.rb +10 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +162 -140
- data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +44 -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 +233 -46
- 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 +78 -65
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +77 -9
- data/lib/better_auth/plugins/organization.rb +546 -102
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +25 -27
- 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 +205 -19
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +300 -133
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +44 -8
- data/lib/better_auth/routes/email_verification.rb +93 -6
- data/lib/better_auth/routes/password.rb +6 -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 +197 -54
- data/lib/better_auth/routes/user.rb +17 -10
- data/lib/better_auth/schema/sql.rb +189 -34
- data/lib/better_auth/schema.rb +159 -5
- data/lib/better_auth/session.rb +29 -10
- 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 +45 -23
- 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/github.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 +566 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +14 -41
- metadata +14 -18
- 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 -215
- data/lib/better_auth/plugins/oidc_provider.rb +0 -710
|
@@ -31,7 +31,7 @@ module BetterAuth
|
|
|
31
31
|
}.freeze
|
|
32
32
|
|
|
33
33
|
attr_reader :app_name,
|
|
34
|
-
:
|
|
34
|
+
:base_url,
|
|
35
35
|
:base_path,
|
|
36
36
|
:context_base_url,
|
|
37
37
|
:secret,
|
|
@@ -39,6 +39,7 @@ module BetterAuth
|
|
|
39
39
|
:database,
|
|
40
40
|
:plugins,
|
|
41
41
|
:trusted_origins,
|
|
42
|
+
:serving_origins,
|
|
42
43
|
:rate_limit,
|
|
43
44
|
:session,
|
|
44
45
|
:account,
|
|
@@ -48,6 +49,7 @@ module BetterAuth
|
|
|
48
49
|
:email_and_password,
|
|
49
50
|
:password_hasher,
|
|
50
51
|
:email_verification,
|
|
52
|
+
:configured_social_provider_ids,
|
|
51
53
|
:social_providers,
|
|
52
54
|
:experimental,
|
|
53
55
|
:secondary_storage,
|
|
@@ -60,6 +62,7 @@ module BetterAuth
|
|
|
60
62
|
:logger
|
|
61
63
|
|
|
62
64
|
def initialize(options = {})
|
|
65
|
+
@configured_social_provider_ids = extract_configured_social_provider_ids(options)
|
|
63
66
|
options = symbolize_keys(options)
|
|
64
67
|
@explicit_options = deep_dup(options)
|
|
65
68
|
|
|
@@ -75,7 +78,7 @@ module BetterAuth
|
|
|
75
78
|
@hooks = options[:hooks]
|
|
76
79
|
@on_api_error = symbolize_keys(options[:on_api_error] || options[:on_apierror] || {})
|
|
77
80
|
@telemetry = symbolize_keys(options[:telemetry] || {})
|
|
78
|
-
@social_providers =
|
|
81
|
+
@social_providers = normalize_social_providers(options[:social_providers])
|
|
79
82
|
@trusted_origins_callbacks = []
|
|
80
83
|
@trusted_origins_callbacks << options[:trusted_origins] if options[:trusted_origins].respond_to?(:call)
|
|
81
84
|
@trusted_origins_callback = combined_trusted_origins_callback
|
|
@@ -88,8 +91,8 @@ module BetterAuth
|
|
|
88
91
|
@secret = legacy_secret || (test_environment? ? DEFAULT_SECRET : nil)
|
|
89
92
|
@secret_config = @secret
|
|
90
93
|
end
|
|
91
|
-
@base_url_config = options[:base_url]
|
|
92
94
|
@base_url, @context_base_url = normalize_base_url(options[:base_url])
|
|
95
|
+
@serving_origins = normalize_serving_origins(options[:serving_origins])
|
|
93
96
|
@session = normalize_session(options[:session])
|
|
94
97
|
@account = normalize_account(options[:account])
|
|
95
98
|
@user = symbolize_keys(options[:user] || {})
|
|
@@ -110,26 +113,17 @@ module BetterAuth
|
|
|
110
113
|
end
|
|
111
114
|
end
|
|
112
115
|
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Thread.current[base_url_runtime_key] = value
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def clear_runtime_base_url!
|
|
122
|
-
Thread.current[base_url_runtime_key] = nil
|
|
116
|
+
def explicit_trusted_origins
|
|
117
|
+
configured = @explicit_options[:trusted_origins]
|
|
118
|
+
origins = configured.respond_to?(:call) ? [] : Array(configured).compact
|
|
119
|
+
origins.concat(env_trusted_origins)
|
|
120
|
+
origins.map(&:to_s).reject(&:empty?).uniq
|
|
123
121
|
end
|
|
124
122
|
|
|
125
123
|
def production?
|
|
126
124
|
production_environment?
|
|
127
125
|
end
|
|
128
126
|
|
|
129
|
-
def dynamic_base_url?
|
|
130
|
-
URLHelpers.dynamic_config?(base_url_config)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
127
|
def to_h
|
|
134
128
|
{
|
|
135
129
|
app_name: app_name,
|
|
@@ -140,6 +134,7 @@ module BetterAuth
|
|
|
140
134
|
database: database,
|
|
141
135
|
plugins: plugins,
|
|
142
136
|
trusted_origins: trusted_origins,
|
|
137
|
+
serving_origins: serving_origins,
|
|
143
138
|
rate_limit: rate_limit,
|
|
144
139
|
session: session,
|
|
145
140
|
account: account,
|
|
@@ -164,7 +159,7 @@ module BetterAuth
|
|
|
164
159
|
normalized = symbolize_keys(defaults || {})
|
|
165
160
|
normalized.each do |key, value|
|
|
166
161
|
next unless respond_to?(key)
|
|
167
|
-
next if key == :database_hooks
|
|
162
|
+
next if key == :database_hooks || key == :configured_social_provider_ids
|
|
168
163
|
|
|
169
164
|
if key == :trusted_origins
|
|
170
165
|
merge_trusted_origins_default(value)
|
|
@@ -231,15 +226,12 @@ module BetterAuth
|
|
|
231
226
|
|
|
232
227
|
def normalize_base_url(value)
|
|
233
228
|
configured = value || env_base_url
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
validate_dynamic_base_url!(configured)
|
|
238
|
-
resolved = URLHelpers.resolve_base_url(configured, base_path)
|
|
239
|
-
return ["", ""] unless resolved
|
|
229
|
+
if configured.nil? || configured.to_s.empty?
|
|
230
|
+
raise Error, "base_url is required. Pass a canonical URL to BetterAuth.auth(base_url: ...) or set BETTER_AUTH_URL."
|
|
231
|
+
end
|
|
240
232
|
|
|
241
|
-
|
|
242
|
-
|
|
233
|
+
if configured.is_a?(Hash)
|
|
234
|
+
raise Error, "Dynamic base_url hashes are no longer supported. Configure a canonical base_url and list request-facing origins in serving_origins."
|
|
243
235
|
end
|
|
244
236
|
|
|
245
237
|
with_path = append_base_path(configured.to_s)
|
|
@@ -250,11 +242,6 @@ module BetterAuth
|
|
|
250
242
|
raise Error, "Invalid base URL: #{configured}. Please provide a valid base URL."
|
|
251
243
|
end
|
|
252
244
|
|
|
253
|
-
def validate_dynamic_base_url!(value)
|
|
254
|
-
allowed_hosts = value[:allowed_hosts] || value["allowed_hosts"] || value[:allowedHosts] || value["allowedHosts"]
|
|
255
|
-
raise Error, "baseURL.allowedHosts cannot be empty" if allowed_hosts.respond_to?(:empty?) && allowed_hosts.empty?
|
|
256
|
-
end
|
|
257
|
-
|
|
258
245
|
def normalize_base_path(value)
|
|
259
246
|
return "" if value.nil? || value == "" || value == "/"
|
|
260
247
|
|
|
@@ -368,27 +355,45 @@ module BetterAuth
|
|
|
368
355
|
end
|
|
369
356
|
|
|
370
357
|
def normalize_plugins(value)
|
|
371
|
-
Array(value).compact.reject { |plugin| plugin == false }.map
|
|
358
|
+
Array(value).compact.reject { |plugin| plugin == false }.map do |plugin|
|
|
359
|
+
coerced = Plugin.coerce(plugin)
|
|
360
|
+
Plugins.ensure_plugin_loaded_for!(coerced)
|
|
361
|
+
coerced
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def normalize_social_providers(value)
|
|
366
|
+
symbolize_keys(value || {}).reject do |_id, provider|
|
|
367
|
+
provider.nil? || provider == false || (provider.is_a?(Hash) && provider[:enabled] == false)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def extract_configured_social_provider_ids(options)
|
|
372
|
+
return [].freeze unless options.is_a?(Hash)
|
|
373
|
+
|
|
374
|
+
options.each_with_object([]) do |(key, providers), ids|
|
|
375
|
+
next unless normalize_key(key) == :social_providers && providers.is_a?(Hash)
|
|
376
|
+
|
|
377
|
+
ids.concat(providers.keys.map { |provider_id| provider_id.to_s.dup.freeze })
|
|
378
|
+
end.uniq.freeze
|
|
372
379
|
end
|
|
373
380
|
|
|
374
381
|
def normalize_trusted_origins(value)
|
|
375
|
-
origins =
|
|
376
|
-
origins << base_url unless base_url.nil? || base_url.empty?
|
|
377
|
-
origins.concat(dynamic_base_url_trusted_origins)
|
|
382
|
+
origins = serving_origins.dup
|
|
378
383
|
origins.concat(Array(value).compact) unless value.respond_to?(:call)
|
|
379
384
|
origins.concat(env_trusted_origins)
|
|
380
385
|
origins.map(&:to_s).reject(&:empty?).uniq
|
|
381
386
|
end
|
|
382
387
|
|
|
383
|
-
def
|
|
384
|
-
|
|
388
|
+
def normalize_serving_origins(value)
|
|
389
|
+
([base_url] + Array(value).compact).map do |origin|
|
|
390
|
+
normalized = origin.to_s.sub(%r{/+\z}, "")
|
|
391
|
+
unless normalized.match?(%r{\Ahttps?://[^/?#@]+\z}i)
|
|
392
|
+
raise Error, "Invalid serving origin: #{origin}. serving_origins entries must be full http(s) origins without a path."
|
|
393
|
+
end
|
|
385
394
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
Array(allowed_hosts).map do |host|
|
|
389
|
-
host = host.to_s
|
|
390
|
-
host.match?(%r{\Ahttps?://}i) ? host : "#{protocol}://#{host}"
|
|
391
|
-
end
|
|
395
|
+
normalized
|
|
396
|
+
end.uniq
|
|
392
397
|
end
|
|
393
398
|
|
|
394
399
|
def merge_trusted_origins_default(value)
|
|
@@ -473,10 +478,6 @@ module BetterAuth
|
|
|
473
478
|
end
|
|
474
479
|
end
|
|
475
480
|
|
|
476
|
-
def base_url_runtime_key
|
|
477
|
-
:"better_auth_configuration_base_url_#{object_id}"
|
|
478
|
-
end
|
|
479
|
-
|
|
480
481
|
def test_environment?
|
|
481
482
|
ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test" || ENV["APP_ENV"] == "test"
|
|
482
483
|
end
|
data/lib/better_auth/context.rb
CHANGED
|
@@ -18,7 +18,8 @@ module BetterAuth
|
|
|
18
18
|
|
|
19
19
|
def initialize(configuration)
|
|
20
20
|
@app_name = configuration.app_name
|
|
21
|
-
@
|
|
21
|
+
@canonical_base_url = configuration.context_base_url
|
|
22
|
+
@base_url = @canonical_base_url
|
|
22
23
|
@version = BetterAuth::VERSION
|
|
23
24
|
@options = configuration
|
|
24
25
|
@social_providers = configuration.social_providers
|
|
@@ -30,6 +31,7 @@ module BetterAuth
|
|
|
30
31
|
@session_config = configuration.session
|
|
31
32
|
@rate_limit_config = configuration.rate_limit
|
|
32
33
|
@trusted_origins = configuration.trusted_origins
|
|
34
|
+
@explicit_trusted_origins = configuration.explicit_trusted_origins
|
|
33
35
|
@secret = configuration.secret
|
|
34
36
|
@secret_config = configuration.secret_config
|
|
35
37
|
@current_session = nil
|
|
@@ -46,10 +48,20 @@ module BetterAuth
|
|
|
46
48
|
runtime_fetch(:base_url, @base_url)
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
def token_link_base_url
|
|
52
|
+
base_url
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr_reader :canonical_base_url
|
|
56
|
+
|
|
49
57
|
def trusted_origins
|
|
50
58
|
runtime_fetch(:trusted_origins, @trusted_origins)
|
|
51
59
|
end
|
|
52
60
|
|
|
61
|
+
def explicit_trusted_origins
|
|
62
|
+
runtime_fetch(:explicit_trusted_origins, @explicit_trusted_origins)
|
|
63
|
+
end
|
|
64
|
+
|
|
53
65
|
def auth_cookies
|
|
54
66
|
runtime_fetch(:auth_cookies, @auth_cookies)
|
|
55
67
|
end
|
|
@@ -131,6 +143,7 @@ module BetterAuth
|
|
|
131
143
|
@session_config = options.session
|
|
132
144
|
@rate_limit_config = options.rate_limit
|
|
133
145
|
@trusted_origins = options.trusted_origins
|
|
146
|
+
@explicit_trusted_origins = options.explicit_trusted_origins
|
|
134
147
|
@secret = options.secret
|
|
135
148
|
@secret_config = options.secret_config
|
|
136
149
|
end
|
|
@@ -150,12 +163,7 @@ module BetterAuth
|
|
|
150
163
|
runtime = request_runtime
|
|
151
164
|
runtime[:current_session] = nil
|
|
152
165
|
runtime[:new_session] = nil
|
|
153
|
-
|
|
154
|
-
runtime[:base_url] = resolved_dynamic_base_url(request)
|
|
155
|
-
refresh_cookies!
|
|
156
|
-
elsif options.base_url.to_s.empty?
|
|
157
|
-
runtime[:base_url] = inferred_base_url(request)
|
|
158
|
-
end
|
|
166
|
+
runtime[:base_url] = serving_base_url(request)
|
|
159
167
|
runtime[:trusted_origins] = current_trusted_origins(request)
|
|
160
168
|
end
|
|
161
169
|
|
|
@@ -163,61 +171,30 @@ module BetterAuth
|
|
|
163
171
|
runtime = request_runtime
|
|
164
172
|
runtime[:current_session] = nil
|
|
165
173
|
runtime[:new_session] = nil
|
|
166
|
-
|
|
167
|
-
runtime[:base_url] = resolved_dynamic_base_url(source)
|
|
168
|
-
refresh_cookies!
|
|
169
|
-
end
|
|
174
|
+
runtime[:base_url] = serving_base_url(source)
|
|
170
175
|
runtime[:trusted_origins] = current_trusted_origins(request_for_callbacks(source))
|
|
171
176
|
end
|
|
172
177
|
|
|
173
178
|
def reset_runtime!
|
|
174
179
|
Thread.current[runtime_key] = nil if request_runtime?
|
|
175
|
-
options.clear_runtime_base_url! if options.respond_to?(:clear_runtime_base_url!)
|
|
176
180
|
@current_session = nil
|
|
177
181
|
@new_session = nil
|
|
178
182
|
end
|
|
179
183
|
|
|
180
184
|
def clear_runtime!
|
|
181
185
|
Thread.current[runtime_key] = nil
|
|
182
|
-
options.clear_runtime_base_url! if options.respond_to?(:clear_runtime_base_url!)
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def refresh_cookies!
|
|
186
|
-
cookies = Cookies.get_cookies(options)
|
|
187
|
-
if request_runtime?
|
|
188
|
-
runtime_store(:auth_cookies, cookies)
|
|
189
|
-
runtime_store(:cookies, cookies)
|
|
190
|
-
else
|
|
191
|
-
@auth_cookies = cookies
|
|
192
|
-
@cookies = cookies
|
|
193
|
-
end
|
|
194
186
|
end
|
|
195
187
|
|
|
196
188
|
private
|
|
197
189
|
|
|
198
|
-
def
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def resolved_dynamic_base_url(request)
|
|
205
|
-
resolved = URLHelpers.resolve_base_url(
|
|
206
|
-
options.base_url_config,
|
|
207
|
-
options.base_path,
|
|
208
|
-
request,
|
|
209
|
-
load_env: true,
|
|
210
|
-
trusted_proxy_headers: dynamic_trusted_proxy_headers?
|
|
211
|
-
)
|
|
212
|
-
origin = Configuration.origin_for(URI.parse(resolved))
|
|
213
|
-
options.set_runtime_base_url(origin) if options.respond_to?(:set_runtime_base_url)
|
|
214
|
-
resolved
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def dynamic_trusted_proxy_headers?
|
|
218
|
-
return true unless options.advanced.key?(:trusted_proxy_headers)
|
|
190
|
+
def serving_base_url(source)
|
|
191
|
+
candidate = inferred_origin(source)
|
|
192
|
+
return canonical_base_url unless candidate
|
|
193
|
+
return canonical_base_url if candidate == options.base_url
|
|
194
|
+
return canonical_base_url unless options.serving_origins.any? { |pattern| Configuration.matches_origin_pattern?(candidate, pattern) }
|
|
219
195
|
|
|
220
|
-
|
|
196
|
+
path = options.base_path
|
|
197
|
+
path.empty? ? candidate : "#{candidate}#{path}"
|
|
221
198
|
end
|
|
222
199
|
|
|
223
200
|
def request_for_callbacks(source)
|
|
@@ -230,6 +207,7 @@ module BetterAuth
|
|
|
230
207
|
Thread.current[runtime_key] ||= {
|
|
231
208
|
base_url: @base_url,
|
|
232
209
|
trusted_origins: @trusted_origins,
|
|
210
|
+
explicit_trusted_origins: @explicit_trusted_origins,
|
|
233
211
|
auth_cookies: @auth_cookies,
|
|
234
212
|
cookies: @cookies,
|
|
235
213
|
current_session: nil,
|
|
@@ -256,26 +234,25 @@ module BetterAuth
|
|
|
256
234
|
:"better_auth_context_runtime_#{object_id}"
|
|
257
235
|
end
|
|
258
236
|
|
|
259
|
-
def inferred_origin(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
237
|
+
def inferred_origin(source)
|
|
238
|
+
return nil unless source
|
|
239
|
+
|
|
240
|
+
trusted_proxy_headers = options.advanced[:trusted_proxy_headers] == true
|
|
241
|
+
forwarded_host = URLHelpers.header_value(URLHelpers.headers_from_source(source), "x-forwarded-host")
|
|
242
|
+
if trusted_proxy_headers && forwarded_host
|
|
243
|
+
forwarded_proto = URLHelpers.header_value(URLHelpers.headers_from_source(source), "x-forwarded-proto")
|
|
244
|
+
return nil unless valid_proxy_host?(forwarded_host.to_s) && valid_proxy_proto?(forwarded_proto.to_s)
|
|
245
|
+
|
|
263
246
|
return "#{forwarded_proto}://#{forwarded_host}"
|
|
264
247
|
end
|
|
265
248
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
host_header = request.get_header("HTTP_HOST")
|
|
269
|
-
return "#{scheme}://#{host_header}" if host_header && valid_proxy_host?(host_header.to_s)
|
|
249
|
+
host = URLHelpers.host_from_source(source, trusted_proxy_headers: false)
|
|
250
|
+
return nil unless host && valid_proxy_host?(host)
|
|
270
251
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
default_port = (scheme == "http" && port == 80) || (scheme == "https" && port == 443)
|
|
274
|
-
default_port ? "#{scheme}://#{host}" : "#{scheme}://#{host}:#{port}"
|
|
275
|
-
end
|
|
252
|
+
protocol = URLHelpers.protocol_from_source(source, trusted_proxy_headers: false)
|
|
253
|
+
return nil unless valid_proxy_proto?(protocol)
|
|
276
254
|
|
|
277
|
-
|
|
278
|
-
valid_proxy_proto?(proto.to_s) && valid_proxy_host?(host.to_s)
|
|
255
|
+
"#{protocol}://#{host}"
|
|
279
256
|
end
|
|
280
257
|
|
|
281
258
|
def valid_proxy_proto?(proto)
|
|
@@ -315,16 +292,15 @@ module BetterAuth
|
|
|
315
292
|
end
|
|
316
293
|
|
|
317
294
|
def current_trusted_origins(request)
|
|
318
|
-
origins =
|
|
319
|
-
|
|
320
|
-
origins.concat(options.trusted_origins)
|
|
295
|
+
origins = options.trusted_origins.dup
|
|
296
|
+
dynamic_origins = []
|
|
321
297
|
if options.trusted_origins_callback
|
|
322
|
-
|
|
298
|
+
dynamic_origins.concat(Array(options.trusted_origins_callback.call(request)).compact)
|
|
323
299
|
end
|
|
300
|
+
origins.concat(dynamic_origins)
|
|
324
301
|
origins.concat(Env.csv("BETTER_AUTH_TRUSTED_ORIGINS"))
|
|
302
|
+
runtime_store(:explicit_trusted_origins, (options.explicit_trusted_origins + dynamic_origins.map(&:to_s)).uniq)
|
|
325
303
|
origins.map(&:to_s).reject(&:empty?).uniq
|
|
326
|
-
rescue URI::InvalidURIError
|
|
327
|
-
options.trusted_origins
|
|
328
304
|
end
|
|
329
305
|
|
|
330
306
|
def normalize_context(value)
|
data/lib/better_auth/cookies.rb
CHANGED
|
@@ -8,8 +8,10 @@ module BetterAuth
|
|
|
8
8
|
module Cookies
|
|
9
9
|
SECURE_COOKIE_PREFIX = "__Secure-"
|
|
10
10
|
HOST_COOKIE_PREFIX = "__Host-"
|
|
11
|
+
COOKIE_NAME_PATTERN = /\A[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E\x5F\x60\x61-\x7A\x7C\x7E]+\z/
|
|
12
|
+
COOKIE_VALUE_PATTERN = /\A[\x20\x21\x23-\x3A\x3C-\x5B\x5D-\x7E]*\z/
|
|
11
13
|
|
|
12
|
-
Cookie = Struct.new(:name, :attributes
|
|
14
|
+
Cookie = Struct.new(:name, :attributes) do
|
|
13
15
|
alias_method :options, :attributes
|
|
14
16
|
end
|
|
15
17
|
|
|
@@ -40,8 +42,6 @@ module BetterAuth
|
|
|
40
42
|
uri.host unless uri.host.to_s.empty?
|
|
41
43
|
end
|
|
42
44
|
end
|
|
43
|
-
raise Error, "base_url is required when cross_subdomain_cookies are enabled" if cross_subdomain && domain.to_s.empty? && !options.dynamic_base_url?
|
|
44
|
-
|
|
45
45
|
custom = advanced.dig(:cookies, cookie_name.to_sym) || {}
|
|
46
46
|
prefix = advanced[:cookie_prefix] || "better-auth"
|
|
47
47
|
name = custom[:name] || "#{prefix}.#{cookie_name}"
|
|
@@ -63,14 +63,51 @@ module BetterAuth
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def parse_cookies(cookie_header)
|
|
66
|
-
cookie_header.to_s
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
header = cookie_header.to_s
|
|
67
|
+
return {} if header.length < 2
|
|
68
|
+
|
|
69
|
+
header.split(";", -1).each_with_object({}) do |pair, result|
|
|
70
|
+
separator = pair.index("=")
|
|
71
|
+
next unless separator
|
|
72
|
+
|
|
73
|
+
name = trim_cookie_ows(pair[0...separator])
|
|
74
|
+
value = unquote_cookie_value(trim_cookie_ows(pair[(separator + 1)..]))
|
|
75
|
+
next unless COOKIE_NAME_PATTERN.match?(name) && COOKIE_VALUE_PATTERN.match?(value)
|
|
69
76
|
|
|
70
|
-
result[name
|
|
77
|
+
result[name] = decode_cookie_value(value)
|
|
71
78
|
end
|
|
72
79
|
end
|
|
73
80
|
|
|
81
|
+
def split_set_cookie_header(header)
|
|
82
|
+
Array(header).flat_map do |value|
|
|
83
|
+
value.to_s.lines(chomp: true).flat_map do |line|
|
|
84
|
+
line.split(/,(?=[ \t]*[!#$%&'*+\-.^_`|~0-9A-Za-z]+=)/).map(&:strip)
|
|
85
|
+
end
|
|
86
|
+
end.reject(&:empty?)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_set_cookie(line)
|
|
90
|
+
first, *attributes = line.to_s.split(";").map(&:strip)
|
|
91
|
+
name, value = first.split("=", 2)
|
|
92
|
+
return unless name && value
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
name: name,
|
|
96
|
+
value: value,
|
|
97
|
+
attributes: attributes.each_with_object({}) do |attribute, result|
|
|
98
|
+
key, attribute_value = attribute.split("=", 2)
|
|
99
|
+
result[key.to_s.downcase] = attribute_value || true unless key.to_s.empty?
|
|
100
|
+
end
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def set_request_cookie(cookie_header, name, value)
|
|
105
|
+
cookies = parse_cookies(cookie_header)
|
|
106
|
+
cookie_name = name.to_s
|
|
107
|
+
cookies[cookie_name] = value.to_s if COOKIE_NAME_PATTERN.match?(cookie_name)
|
|
108
|
+
cookies.map { |key, semantic_value| "#{key}=#{encode_cookie_value(semantic_value)}" }.join("; ")
|
|
109
|
+
end
|
|
110
|
+
|
|
74
111
|
def strip_secure_cookie_prefix(name)
|
|
75
112
|
name.to_s.delete_prefix(SECURE_COOKIE_PREFIX).delete_prefix(HOST_COOKIE_PREFIX)
|
|
76
113
|
end
|
|
@@ -83,10 +120,10 @@ module BetterAuth
|
|
|
83
120
|
cookie_name = config[:cookie_name] || "session_token"
|
|
84
121
|
cookie_prefix = config[:cookie_prefix] || "better-auth"
|
|
85
122
|
candidates = [
|
|
86
|
-
"#{cookie_prefix}.#{cookie_name}",
|
|
87
123
|
"#{SECURE_COOKIE_PREFIX}#{cookie_prefix}.#{cookie_name}",
|
|
88
|
-
"#{cookie_prefix}
|
|
89
|
-
"#{SECURE_COOKIE_PREFIX}#{cookie_prefix}-#{cookie_name}"
|
|
124
|
+
"#{cookie_prefix}.#{cookie_name}",
|
|
125
|
+
"#{SECURE_COOKIE_PREFIX}#{cookie_prefix}-#{cookie_name}",
|
|
126
|
+
"#{cookie_prefix}-#{cookie_name}"
|
|
90
127
|
]
|
|
91
128
|
candidates.lazy.filter_map { |candidate| parsed[candidate] }.first
|
|
92
129
|
end
|
|
@@ -101,16 +138,23 @@ module BetterAuth
|
|
|
101
138
|
ctx.set_signed_cookie(dont_remember_cookie.name, "true", ctx.context.secret, dont_remember_cookie.attributes)
|
|
102
139
|
end
|
|
103
140
|
|
|
104
|
-
set_cookie_cache(ctx, session, dont_remember_me)
|
|
141
|
+
set_cookie_cache(ctx, session, dont_remember_me, max_age: overrides&.fetch(:max_age, nil))
|
|
105
142
|
ctx.context.set_new_session(session) if ctx.context.respond_to?(:set_new_session)
|
|
106
143
|
end
|
|
107
144
|
|
|
108
|
-
def set_cookie_cache(ctx, session, dont_remember_me)
|
|
145
|
+
def set_cookie_cache(ctx, session, dont_remember_me, max_age: nil)
|
|
109
146
|
config = ctx.context.session_config[:cookie_cache] || {}
|
|
110
147
|
return unless config[:enabled]
|
|
111
148
|
|
|
112
149
|
cookie = ctx.context.auth_cookies[:session_data]
|
|
113
|
-
|
|
150
|
+
configured_max_age = cookie.attributes[:max_age]
|
|
151
|
+
max_age = if dont_remember_me
|
|
152
|
+
nil
|
|
153
|
+
elsif max_age
|
|
154
|
+
[configured_max_age, max_age].compact.map(&:to_i).min
|
|
155
|
+
else
|
|
156
|
+
configured_max_age
|
|
157
|
+
end
|
|
114
158
|
data = filtered_cache_data(ctx, session)
|
|
115
159
|
strategy = config[:strategy] || "compact"
|
|
116
160
|
secret = (strategy.to_s == "jwe") ? ctx.context.secret_config : ctx.context.secret
|
|
@@ -150,12 +194,14 @@ module BetterAuth
|
|
|
150
194
|
Crypto.symmetric_decode_jwt(value, ctx.context.secret_config, "better-auth-account")
|
|
151
195
|
end
|
|
152
196
|
|
|
153
|
-
def get_cookie_cache(request_or_cookie_header, secret:, strategy: "compact", version: nil, cookie_prefix: "better-auth", cookie_name: "session_data", is_secure: nil)
|
|
197
|
+
def get_cookie_cache(request_or_cookie_header, secret:, strategy: "compact", version: nil, cookie_prefix: "better-auth", cookie_name: "session_data", is_secure: nil, cookie_full_name: nil)
|
|
154
198
|
cookie_header = header_value(request_or_cookie_header)
|
|
155
199
|
return nil if cookie_header.to_s.empty?
|
|
156
200
|
|
|
157
201
|
parsed = parse_cookies(cookie_header)
|
|
158
|
-
name = if
|
|
202
|
+
name = if cookie_full_name
|
|
203
|
+
cookie_full_name
|
|
204
|
+
elsif is_secure.nil?
|
|
159
205
|
production_environment? ? "#{SECURE_COOKIE_PREFIX}#{cookie_prefix}.#{cookie_name}" : "#{cookie_prefix}.#{cookie_name}"
|
|
160
206
|
else
|
|
161
207
|
secure_prefix = is_secure ? SECURE_COOKIE_PREFIX : ""
|
|
@@ -166,6 +212,7 @@ module BetterAuth
|
|
|
166
212
|
|
|
167
213
|
payload = decode_cookie_cache(raw, secret, strategy: strategy)
|
|
168
214
|
return nil unless payload && payload["session"] && payload["user"]
|
|
215
|
+
return nil if embedded_session_expired?(payload["session"])
|
|
169
216
|
|
|
170
217
|
expected_version = cookie_cache_version(version, payload["session"], payload["user"])
|
|
171
218
|
return nil if version && (payload["version"] || "1") != expected_version
|
|
@@ -174,6 +221,7 @@ module BetterAuth
|
|
|
174
221
|
end
|
|
175
222
|
|
|
176
223
|
def expire_cookie(ctx, cookie)
|
|
224
|
+
remove_set_cookie_entries(ctx, cookie.name)
|
|
177
225
|
ctx.set_cookie(cookie.name, "", cookie.attributes.merge(max_age: 0))
|
|
178
226
|
end
|
|
179
227
|
|
|
@@ -182,6 +230,11 @@ module BetterAuth
|
|
|
182
230
|
expire_cookie(ctx, ctx.context.auth_cookies[:session_data])
|
|
183
231
|
expire_cookie(ctx, ctx.context.auth_cookies[:account_data]) if ctx.context.options.account[:store_account_cookie]
|
|
184
232
|
|
|
233
|
+
if ctx.context.options.account[:store_account_cookie]
|
|
234
|
+
account_store = SessionStore.new(ctx.context.auth_cookies[:account_data].name, ctx.context.auth_cookies[:account_data].attributes, ctx)
|
|
235
|
+
account_store.set_cookies(account_store.clean)
|
|
236
|
+
end
|
|
237
|
+
|
|
185
238
|
store = SessionStore.new(ctx.context.auth_cookies[:session_data].name, ctx.context.auth_cookies[:session_data].attributes, ctx)
|
|
186
239
|
store.set_cookies(store.clean)
|
|
187
240
|
expire_cookie(ctx, ctx.context.auth_cookies[:dont_remember]) unless skip_dont_remember_me
|
|
@@ -248,6 +301,57 @@ module BetterAuth
|
|
|
248
301
|
chunks.sort_by(&:first).map(&:last).join
|
|
249
302
|
end
|
|
250
303
|
|
|
304
|
+
def embedded_session_expired?(session)
|
|
305
|
+
expires_at = session["expiresAt"] || session[:expiresAt] || session["expires_at"] || session[:expires_at]
|
|
306
|
+
normalized = Session.normalize_time(expires_at)
|
|
307
|
+
normalized && normalized <= Time.now
|
|
308
|
+
rescue ArgumentError
|
|
309
|
+
true
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def remove_set_cookie_entries(ctx, cookie_name)
|
|
313
|
+
header = ctx.response_headers["set-cookie"]
|
|
314
|
+
return unless header
|
|
315
|
+
|
|
316
|
+
exact = "#{cookie_name}="
|
|
317
|
+
chunk = "#{cookie_name}."
|
|
318
|
+
survivors = header.to_s.lines(chomp: true).reject do |entry|
|
|
319
|
+
entry.start_with?(exact, chunk)
|
|
320
|
+
end
|
|
321
|
+
if survivors.empty?
|
|
322
|
+
ctx.response_headers.delete("set-cookie")
|
|
323
|
+
else
|
|
324
|
+
ctx.response_headers["set-cookie"] = survivors.join("\n")
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def decode_cookie_value(value)
|
|
329
|
+
decoded = URI.decode_uri_component(value)
|
|
330
|
+
decoded.valid_encoding? ? decoded : value
|
|
331
|
+
rescue ArgumentError
|
|
332
|
+
value
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def trim_cookie_ows(value)
|
|
336
|
+
value.sub(/\A[ \t]*/, "").sub(/[ \t]*\z/, "")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def unquote_cookie_value(value)
|
|
340
|
+
return value unless value.length >= 2 && value.start_with?("\"") && value.end_with?("\"")
|
|
341
|
+
|
|
342
|
+
value[1...-1]
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def encode_cookie_value(value)
|
|
346
|
+
value.to_s.encode(Encoding::UTF_8).bytes.map do |byte|
|
|
347
|
+
if byte.between?(48, 57) || byte.between?(65, 90) || byte.between?(97, 122) || [33, 39, 40, 41, 42, 45, 46, 95, 126].include?(byte)
|
|
348
|
+
byte.chr
|
|
349
|
+
else
|
|
350
|
+
"%#{byte.to_s(16).upcase.rjust(2, "0")}"
|
|
351
|
+
end
|
|
352
|
+
end.join
|
|
353
|
+
end
|
|
354
|
+
|
|
251
355
|
def header_value(request_or_cookie_header)
|
|
252
356
|
return request_or_cookie_header.headers["cookie"] if request_or_cookie_header.respond_to?(:headers)
|
|
253
357
|
return request_or_cookie_header.get_header("HTTP_COOKIE") if request_or_cookie_header.respond_to?(:get_header)
|
data/lib/better_auth/core.rb
CHANGED
|
@@ -32,9 +32,7 @@ module BetterAuth
|
|
|
32
32
|
delete_user: Routes.delete_user,
|
|
33
33
|
delete_user_callback: Routes.delete_user_callback,
|
|
34
34
|
list_accounts: Routes.list_accounts,
|
|
35
|
-
list_user_accounts: Routes.list_accounts,
|
|
36
35
|
link_social: Routes.link_social,
|
|
37
|
-
link_social_account: Routes.link_social,
|
|
38
36
|
unlink_account: Routes.unlink_account,
|
|
39
37
|
get_access_token: Routes.get_access_token,
|
|
40
38
|
refresh_token: Routes.refresh_token,
|
data/lib/better_auth/crypto.rb
CHANGED
|
@@ -9,7 +9,8 @@ require_relative "crypto/jwe"
|
|
|
9
9
|
|
|
10
10
|
module BetterAuth
|
|
11
11
|
module Crypto
|
|
12
|
-
|
|
12
|
+
ALPHABETIC_ALPHABET = [*"a".."z", *"A".."Z"].freeze
|
|
13
|
+
URL_SAFE_ALPHABET = [*ALPHABETIC_ALPHABET, *"0".."9", "-", "_"].freeze
|
|
13
14
|
MASK_64 = (1 << 64) - 1
|
|
14
15
|
KECCAK_ROUND_CONSTANTS = [
|
|
15
16
|
0x0000000000000001, 0x0000000000008082, 0x800000000000808a, 0x8000000080008000,
|
|
@@ -29,8 +30,8 @@ module BetterAuth
|
|
|
29
30
|
|
|
30
31
|
module_function
|
|
31
32
|
|
|
32
|
-
def random_string(length = 32)
|
|
33
|
-
Array.new(length) {
|
|
33
|
+
def random_string(length = 32, alphabet: URL_SAFE_ALPHABET)
|
|
34
|
+
Array.new(length) { alphabet[SecureRandom.random_number(alphabet.length)] }.join
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def uuid
|