better_auth 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +24 -0
- data/lib/better_auth/adapters/internal_adapter.rb +10 -7
- data/lib/better_auth/adapters/memory.rb +57 -11
- data/lib/better_auth/adapters/sql.rb +123 -20
- data/lib/better_auth/api.rb +114 -9
- data/lib/better_auth/async.rb +70 -0
- data/lib/better_auth/configuration.rb +97 -7
- data/lib/better_auth/context.rb +165 -12
- data/lib/better_auth/cookies.rb +6 -4
- data/lib/better_auth/core.rb +2 -0
- data/lib/better_auth/crypto/jwe.rb +27 -5
- data/lib/better_auth/crypto.rb +32 -0
- data/lib/better_auth/database_hooks.rb +8 -8
- data/lib/better_auth/deprecate.rb +28 -0
- data/lib/better_auth/endpoint.rb +92 -5
- data/lib/better_auth/error.rb +8 -1
- data/lib/better_auth/host.rb +166 -0
- data/lib/better_auth/instrumentation.rb +74 -0
- data/lib/better_auth/logger.rb +31 -0
- data/lib/better_auth/middleware/origin_check.rb +2 -2
- data/lib/better_auth/oauth2.rb +94 -0
- data/lib/better_auth/plugins/admin/schema.rb +2 -2
- data/lib/better_auth/plugins/admin.rb +344 -16
- data/lib/better_auth/plugins/anonymous.rb +37 -3
- data/lib/better_auth/plugins/device_authorization.rb +102 -5
- data/lib/better_auth/plugins/dub.rb +148 -0
- data/lib/better_auth/plugins/email_otp.rb +261 -19
- data/lib/better_auth/plugins/expo.rb +17 -1
- data/lib/better_auth/plugins/generic_oauth.rb +67 -35
- data/lib/better_auth/plugins/jwt.rb +37 -4
- data/lib/better_auth/plugins/last_login_method.rb +2 -2
- data/lib/better_auth/plugins/magic_link.rb +66 -3
- data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
- data/lib/better_auth/plugins/mcp/config.rb +51 -0
- data/lib/better_auth/plugins/mcp/consent.rb +31 -0
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
- data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
- data/lib/better_auth/plugins/mcp/registration.rb +31 -0
- data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
- data/lib/better_auth/plugins/mcp/schema.rb +91 -0
- data/lib/better_auth/plugins/mcp/token.rb +108 -0
- data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
- data/lib/better_auth/plugins/mcp.rb +111 -263
- data/lib/better_auth/plugins/multi_session.rb +61 -3
- data/lib/better_auth/plugins/oauth_protocol.rb +173 -30
- data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
- data/lib/better_auth/plugins/oidc_provider.rb +118 -14
- data/lib/better_auth/plugins/one_tap.rb +7 -2
- data/lib/better_auth/plugins/one_time_token.rb +42 -2
- data/lib/better_auth/plugins/open_api.rb +163 -318
- data/lib/better_auth/plugins/organization/schema.rb +6 -0
- data/lib/better_auth/plugins/organization.rb +186 -56
- data/lib/better_auth/plugins/phone_number.rb +141 -6
- data/lib/better_auth/plugins/siwe.rb +69 -3
- data/lib/better_auth/plugins/two_factor.rb +118 -41
- data/lib/better_auth/plugins/username.rb +57 -2
- data/lib/better_auth/rate_limiter.rb +38 -0
- data/lib/better_auth/request_state.rb +44 -0
- data/lib/better_auth/response.rb +42 -0
- data/lib/better_auth/router.rb +7 -1
- data/lib/better_auth/routes/account.rb +220 -42
- data/lib/better_auth/routes/email_verification.rb +98 -14
- data/lib/better_auth/routes/password.rb +126 -8
- data/lib/better_auth/routes/session.rb +128 -13
- data/lib/better_auth/routes/sign_in.rb +26 -2
- data/lib/better_auth/routes/sign_out.rb +13 -1
- data/lib/better_auth/routes/sign_up.rb +70 -4
- data/lib/better_auth/routes/social.rb +132 -7
- data/lib/better_auth/routes/user.rb +228 -20
- data/lib/better_auth/routes/validation.rb +50 -0
- data/lib/better_auth/secret_config.rb +115 -0
- data/lib/better_auth/session.rb +13 -2
- data/lib/better_auth/url_helpers.rb +206 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +12 -0
- metadata +23 -1
|
@@ -31,10 +31,11 @@ module BetterAuth
|
|
|
31
31
|
}.freeze
|
|
32
32
|
|
|
33
33
|
attr_reader :app_name,
|
|
34
|
-
:
|
|
34
|
+
:base_url_config,
|
|
35
35
|
:base_path,
|
|
36
36
|
:context_base_url,
|
|
37
37
|
:secret,
|
|
38
|
+
:secret_config,
|
|
38
39
|
:database,
|
|
39
40
|
:plugins,
|
|
40
41
|
:trusted_origins,
|
|
@@ -73,8 +74,19 @@ module BetterAuth
|
|
|
73
74
|
@hooks = options[:hooks]
|
|
74
75
|
@on_api_error = symbolize_keys(options[:on_api_error] || options[:on_apierror] || {})
|
|
75
76
|
@social_providers = symbolize_keys(options[:social_providers] || {})
|
|
76
|
-
@
|
|
77
|
-
@
|
|
77
|
+
@trusted_origins_callbacks = []
|
|
78
|
+
@trusted_origins_callbacks << options[:trusted_origins] if options[:trusted_origins].respond_to?(:call)
|
|
79
|
+
@trusted_origins_callback = combined_trusted_origins_callback
|
|
80
|
+
legacy_secret = resolve_secret(options, allow_test_default: false)
|
|
81
|
+
secrets = options.key?(:secrets) ? options[:secrets] : SecretConfig.parse_env(ENV["BETTER_AUTH_SECRETS"])
|
|
82
|
+
if secrets
|
|
83
|
+
@secret_config = SecretConfig.build(secrets, legacy_secret, logger: logger)
|
|
84
|
+
@secret = @secret_config.current_secret
|
|
85
|
+
else
|
|
86
|
+
@secret = legacy_secret || (test_environment? ? DEFAULT_SECRET : nil)
|
|
87
|
+
@secret_config = @secret
|
|
88
|
+
end
|
|
89
|
+
@base_url_config = options[:base_url]
|
|
78
90
|
@base_url, @context_base_url = normalize_base_url(options[:base_url])
|
|
79
91
|
@session = normalize_session(options[:session])
|
|
80
92
|
@account = normalize_account(options[:account])
|
|
@@ -96,16 +108,33 @@ module BetterAuth
|
|
|
96
108
|
end
|
|
97
109
|
end
|
|
98
110
|
|
|
111
|
+
def base_url
|
|
112
|
+
Thread.current[base_url_runtime_key] || @base_url
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def set_runtime_base_url(value)
|
|
116
|
+
Thread.current[base_url_runtime_key] = value
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def clear_runtime_base_url!
|
|
120
|
+
Thread.current[base_url_runtime_key] = nil
|
|
121
|
+
end
|
|
122
|
+
|
|
99
123
|
def production?
|
|
100
124
|
production_environment?
|
|
101
125
|
end
|
|
102
126
|
|
|
127
|
+
def dynamic_base_url?
|
|
128
|
+
URLHelpers.dynamic_config?(base_url_config)
|
|
129
|
+
end
|
|
130
|
+
|
|
103
131
|
def to_h
|
|
104
132
|
{
|
|
105
133
|
app_name: app_name,
|
|
106
134
|
base_url: base_url,
|
|
107
135
|
base_path: base_path,
|
|
108
136
|
secret: secret,
|
|
137
|
+
secret_config: secret_config,
|
|
109
138
|
database: database,
|
|
110
139
|
plugins: plugins,
|
|
111
140
|
trusted_origins: trusted_origins,
|
|
@@ -134,6 +163,11 @@ module BetterAuth
|
|
|
134
163
|
next unless respond_to?(key)
|
|
135
164
|
next if key == :database_hooks
|
|
136
165
|
|
|
166
|
+
if key == :trusted_origins
|
|
167
|
+
merge_trusted_origins_default(value)
|
|
168
|
+
next
|
|
169
|
+
end
|
|
170
|
+
|
|
137
171
|
instance_variable_set("@#{key}", merge_default_value([key], public_send(key), value))
|
|
138
172
|
end
|
|
139
173
|
end
|
|
@@ -146,7 +180,12 @@ module BetterAuth
|
|
|
146
180
|
return false unless uri
|
|
147
181
|
|
|
148
182
|
if pattern.include?("*") || pattern.include?("?")
|
|
149
|
-
|
|
183
|
+
if pattern.include?("://")
|
|
184
|
+
origin = origin_for(uri)
|
|
185
|
+
return true if origin && wildcard_match?(pattern, origin)
|
|
186
|
+
|
|
187
|
+
return wildcard_match?(pattern, url)
|
|
188
|
+
end
|
|
150
189
|
|
|
151
190
|
return wildcard_match?(pattern, uri.host.to_s)
|
|
152
191
|
end
|
|
@@ -191,6 +230,15 @@ module BetterAuth
|
|
|
191
230
|
configured = value || env_base_url
|
|
192
231
|
return ["", ""] unless configured && !configured.empty?
|
|
193
232
|
|
|
233
|
+
if URLHelpers.dynamic_config?(configured)
|
|
234
|
+
validate_dynamic_base_url!(configured)
|
|
235
|
+
resolved = URLHelpers.resolve_base_url(configured, base_path)
|
|
236
|
+
return ["", ""] unless resolved
|
|
237
|
+
|
|
238
|
+
uri = URI.parse(resolved)
|
|
239
|
+
return [self.class.origin_for(uri), resolved.sub(%r{/+\z}, "")]
|
|
240
|
+
end
|
|
241
|
+
|
|
194
242
|
with_path = append_base_path(configured.to_s)
|
|
195
243
|
uri = URI.parse(with_path)
|
|
196
244
|
validate_http_url!(uri, configured)
|
|
@@ -199,6 +247,11 @@ module BetterAuth
|
|
|
199
247
|
raise Error, "Invalid base URL: #{configured}. Please provide a valid base URL."
|
|
200
248
|
end
|
|
201
249
|
|
|
250
|
+
def validate_dynamic_base_url!(value)
|
|
251
|
+
allowed_hosts = value[:allowed_hosts] || value["allowed_hosts"] || value[:allowedHosts] || value["allowedHosts"]
|
|
252
|
+
raise Error, "baseURL.allowedHosts cannot be empty" if allowed_hosts.respond_to?(:empty?) && allowed_hosts.empty?
|
|
253
|
+
end
|
|
254
|
+
|
|
202
255
|
def normalize_base_path(value)
|
|
203
256
|
return "" if value.nil? || value == "" || value == "/"
|
|
204
257
|
|
|
@@ -235,8 +288,9 @@ module BetterAuth
|
|
|
235
288
|
].find { |value| value && !value.empty? }
|
|
236
289
|
end
|
|
237
290
|
|
|
238
|
-
def resolve_secret(options)
|
|
239
|
-
options[:secret]
|
|
291
|
+
def resolve_secret(options, allow_test_default: true)
|
|
292
|
+
[options[:secret], ENV["BETTER_AUTH_SECRET"], ENV["AUTH_SECRET"]].find { |value| value && !value.empty? } ||
|
|
293
|
+
((allow_test_default && test_environment?) ? DEFAULT_SECRET : nil)
|
|
240
294
|
end
|
|
241
295
|
|
|
242
296
|
def validate_secret
|
|
@@ -244,6 +298,10 @@ module BetterAuth
|
|
|
244
298
|
raise Error, "BETTER_AUTH_SECRET is missing. Set it in your environment or pass `secret` to BetterAuth.auth(secret: ...)."
|
|
245
299
|
end
|
|
246
300
|
|
|
301
|
+
if production_environment? && secret == DEFAULT_SECRET
|
|
302
|
+
raise Error, "You are using the default secret. Please set `BETTER_AUTH_SECRET` in your environment variables or pass `secret` in your auth config."
|
|
303
|
+
end
|
|
304
|
+
|
|
247
305
|
return if test_environment? && secret == DEFAULT_SECRET
|
|
248
306
|
|
|
249
307
|
warn("[better-auth] Warning: your BETTER_AUTH_SECRET should be at least 32 characters long for adequate security.") if secret.length < 32
|
|
@@ -263,7 +321,8 @@ module BetterAuth
|
|
|
263
321
|
session = deep_merge(DEFAULT_SESSION, configured)
|
|
264
322
|
|
|
265
323
|
if database.nil?
|
|
266
|
-
session = deep_merge(session, DEFAULT_STATELESS_SESSION)
|
|
324
|
+
session = deep_merge(session, deep_dup(DEFAULT_STATELESS_SESSION))
|
|
325
|
+
session[:cookie_cache][:max_age] ||= session[:expires_in]
|
|
267
326
|
else
|
|
268
327
|
session[:cookie_cache] = cookie_cache unless cookie_cache.empty?
|
|
269
328
|
end
|
|
@@ -316,11 +375,38 @@ module BetterAuth
|
|
|
316
375
|
def normalize_trusted_origins(value)
|
|
317
376
|
origins = []
|
|
318
377
|
origins << base_url unless base_url.nil? || base_url.empty?
|
|
378
|
+
origins.concat(dynamic_base_url_trusted_origins)
|
|
319
379
|
origins.concat(Array(value).compact) unless value.respond_to?(:call)
|
|
320
380
|
origins.concat(env_trusted_origins)
|
|
321
381
|
origins.map(&:to_s).reject(&:empty?).uniq
|
|
322
382
|
end
|
|
323
383
|
|
|
384
|
+
def dynamic_base_url_trusted_origins
|
|
385
|
+
return [] unless URLHelpers.dynamic_config?(base_url_config)
|
|
386
|
+
|
|
387
|
+
protocol = base_url_config[:protocol] || base_url_config["protocol"] || "https"
|
|
388
|
+
allowed_hosts = base_url_config[:allowed_hosts] || base_url_config["allowed_hosts"] || base_url_config[:allowedHosts] || base_url_config["allowedHosts"] || []
|
|
389
|
+
Array(allowed_hosts).map do |host|
|
|
390
|
+
host = host.to_s
|
|
391
|
+
host.match?(%r{\Ahttps?://}i) ? host : "#{protocol}://#{host}"
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def merge_trusted_origins_default(value)
|
|
396
|
+
if value.respond_to?(:call)
|
|
397
|
+
@trusted_origins_callbacks << value
|
|
398
|
+
@trusted_origins_callback = combined_trusted_origins_callback
|
|
399
|
+
else
|
|
400
|
+
@trusted_origins = (trusted_origins + Array(value).compact.map(&:to_s).reject(&:empty?)).uniq
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def combined_trusted_origins_callback
|
|
405
|
+
return nil if @trusted_origins_callbacks.empty?
|
|
406
|
+
|
|
407
|
+
->(request) { @trusted_origins_callbacks.flat_map { |callback| Array(callback.call(request)) }.compact }
|
|
408
|
+
end
|
|
409
|
+
|
|
324
410
|
def env_trusted_origins
|
|
325
411
|
ENV.fetch("BETTER_AUTH_TRUSTED_ORIGINS", "").split(",").map(&:strip).reject(&:empty?)
|
|
326
412
|
end
|
|
@@ -388,6 +474,10 @@ module BetterAuth
|
|
|
388
474
|
end
|
|
389
475
|
end
|
|
390
476
|
|
|
477
|
+
def base_url_runtime_key
|
|
478
|
+
:"better_auth_configuration_base_url_#{object_id}"
|
|
479
|
+
end
|
|
480
|
+
|
|
391
481
|
def test_environment?
|
|
392
482
|
ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test" || ENV["APP_ENV"] == "test"
|
|
393
483
|
end
|
data/lib/better_auth/context.rb
CHANGED
|
@@ -5,21 +5,16 @@ require "uri"
|
|
|
5
5
|
module BetterAuth
|
|
6
6
|
class Context
|
|
7
7
|
attr_reader :app_name,
|
|
8
|
-
:base_url,
|
|
9
8
|
:version,
|
|
10
9
|
:options,
|
|
11
10
|
:social_providers,
|
|
12
|
-
:cookies,
|
|
13
|
-
:auth_cookies,
|
|
14
11
|
:adapter,
|
|
15
12
|
:internal_adapter,
|
|
16
13
|
:logger,
|
|
17
14
|
:session_config,
|
|
18
15
|
:rate_limit_config,
|
|
19
|
-
:trusted_origins,
|
|
20
16
|
:secret,
|
|
21
|
-
:
|
|
22
|
-
:new_session
|
|
17
|
+
:secret_config
|
|
23
18
|
|
|
24
19
|
def initialize(configuration)
|
|
25
20
|
@app_name = configuration.app_name
|
|
@@ -36,6 +31,7 @@ module BetterAuth
|
|
|
36
31
|
@rate_limit_config = configuration.rate_limit
|
|
37
32
|
@trusted_origins = configuration.trusted_origins
|
|
38
33
|
@secret = configuration.secret
|
|
34
|
+
@secret_config = configuration.secret_config
|
|
39
35
|
@current_session = nil
|
|
40
36
|
@new_session = nil
|
|
41
37
|
end
|
|
@@ -46,12 +42,36 @@ module BetterAuth
|
|
|
46
42
|
end
|
|
47
43
|
end
|
|
48
44
|
|
|
45
|
+
def base_url
|
|
46
|
+
runtime_fetch(:base_url, @base_url)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def trusted_origins
|
|
50
|
+
runtime_fetch(:trusted_origins, @trusted_origins)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def auth_cookies
|
|
54
|
+
runtime_fetch(:auth_cookies, @auth_cookies)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def cookies
|
|
58
|
+
runtime_fetch(:cookies, @cookies)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def current_session
|
|
62
|
+
runtime_fetch(:current_session, @current_session)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def new_session
|
|
66
|
+
runtime_fetch(:new_session, @new_session)
|
|
67
|
+
end
|
|
68
|
+
|
|
49
69
|
def set_new_session(session)
|
|
50
|
-
@new_session = session
|
|
70
|
+
runtime_store(:new_session, session) || @new_session = session
|
|
51
71
|
end
|
|
52
72
|
|
|
53
73
|
def set_current_session(session)
|
|
54
|
-
@current_session = session
|
|
74
|
+
runtime_store(:current_session, session) || @current_session = session
|
|
55
75
|
end
|
|
56
76
|
|
|
57
77
|
def run_in_background(task)
|
|
@@ -63,6 +83,31 @@ module BetterAuth
|
|
|
63
83
|
end
|
|
64
84
|
end
|
|
65
85
|
|
|
86
|
+
def password
|
|
87
|
+
config = {
|
|
88
|
+
min_password_length: options.email_and_password[:min_password_length],
|
|
89
|
+
max_password_length: options.email_and_password[:max_password_length]
|
|
90
|
+
}
|
|
91
|
+
password_config = options.email_and_password[:password] || {}
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
config: config,
|
|
95
|
+
hash: ->(value) { Password.hash(value, hasher: password_config[:hash], algorithm: options.password_hasher) },
|
|
96
|
+
verify: lambda do |password:, hash:|
|
|
97
|
+
Password.verify(
|
|
98
|
+
password: password,
|
|
99
|
+
hash: hash,
|
|
100
|
+
verifier: password_config[:verify],
|
|
101
|
+
algorithm: options.password_hasher
|
|
102
|
+
)
|
|
103
|
+
end,
|
|
104
|
+
check_password: lambda do |value|
|
|
105
|
+
length = value.to_s.length
|
|
106
|
+
length.between?(config[:min_password_length].to_i, config[:max_password_length].to_i)
|
|
107
|
+
end
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
66
111
|
def create_auth_cookie(cookie_name, override_attributes = {})
|
|
67
112
|
Cookies.create_cookie(options, cookie_name.to_s, override_attributes)
|
|
68
113
|
end
|
|
@@ -87,6 +132,7 @@ module BetterAuth
|
|
|
87
132
|
@rate_limit_config = options.rate_limit
|
|
88
133
|
@trusted_origins = options.trusted_origins
|
|
89
134
|
@secret = options.secret
|
|
135
|
+
@secret_config = options.secret_config
|
|
90
136
|
end
|
|
91
137
|
|
|
92
138
|
def method_missing(name, *arguments, &block)
|
|
@@ -101,17 +147,52 @@ module BetterAuth
|
|
|
101
147
|
end
|
|
102
148
|
|
|
103
149
|
def prepare_for_request!(request)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
runtime = request_runtime
|
|
151
|
+
runtime[:current_session] = nil
|
|
152
|
+
runtime[:new_session] = nil
|
|
153
|
+
if options.dynamic_base_url?
|
|
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
|
|
159
|
+
runtime[:trusted_origins] = current_trusted_origins(request)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def prepare_for_api_call!(source)
|
|
163
|
+
runtime = request_runtime
|
|
164
|
+
runtime[:current_session] = nil
|
|
165
|
+
runtime[:new_session] = nil
|
|
166
|
+
if options.dynamic_base_url?
|
|
167
|
+
runtime[:base_url] = resolved_dynamic_base_url(source)
|
|
168
|
+
refresh_cookies!
|
|
169
|
+
end
|
|
170
|
+
runtime[:trusted_origins] = current_trusted_origins(request_for_callbacks(source))
|
|
108
171
|
end
|
|
109
172
|
|
|
110
173
|
def reset_runtime!
|
|
174
|
+
Thread.current[runtime_key] = nil if request_runtime?
|
|
175
|
+
options.clear_runtime_base_url! if options.respond_to?(:clear_runtime_base_url!)
|
|
111
176
|
@current_session = nil
|
|
112
177
|
@new_session = nil
|
|
113
178
|
end
|
|
114
179
|
|
|
180
|
+
def clear_runtime!
|
|
181
|
+
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
|
+
end
|
|
195
|
+
|
|
115
196
|
private
|
|
116
197
|
|
|
117
198
|
def inferred_base_url(request)
|
|
@@ -120,6 +201,61 @@ module BetterAuth
|
|
|
120
201
|
path.empty? ? origin : "#{origin}#{path}"
|
|
121
202
|
end
|
|
122
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)
|
|
219
|
+
|
|
220
|
+
!!options.advanced[:trusted_proxy_headers]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def request_for_callbacks(source)
|
|
224
|
+
return source if source.respond_to?(:get_header)
|
|
225
|
+
|
|
226
|
+
DirectAPIRequest.new(source, base_url) if source.is_a?(Hash)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def request_runtime
|
|
230
|
+
Thread.current[runtime_key] ||= {
|
|
231
|
+
base_url: @base_url,
|
|
232
|
+
trusted_origins: @trusted_origins,
|
|
233
|
+
auth_cookies: @auth_cookies,
|
|
234
|
+
cookies: @cookies,
|
|
235
|
+
current_session: nil,
|
|
236
|
+
new_session: nil
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def request_runtime?
|
|
241
|
+
!!Thread.current[runtime_key]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def runtime_fetch(key, fallback)
|
|
245
|
+
request_runtime? ? Thread.current[runtime_key].fetch(key, fallback) : fallback
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def runtime_store(key, value)
|
|
249
|
+
return false unless request_runtime?
|
|
250
|
+
|
|
251
|
+
Thread.current[runtime_key][key] = value
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def runtime_key
|
|
256
|
+
:"better_auth_context_runtime_#{object_id}"
|
|
257
|
+
end
|
|
258
|
+
|
|
123
259
|
def inferred_origin(request)
|
|
124
260
|
forwarded_host = request.get_header("HTTP_X_FORWARDED_HOST")
|
|
125
261
|
forwarded_proto = request.get_header("HTTP_X_FORWARDED_PROTO")
|
|
@@ -207,5 +343,22 @@ module BetterAuth
|
|
|
207
343
|
def plugin_context_attribute?(key)
|
|
208
344
|
![:options, :adapter, :internal_adapter].include?(key)
|
|
209
345
|
end
|
|
346
|
+
|
|
347
|
+
class DirectAPIRequest
|
|
348
|
+
attr_reader :headers, :url
|
|
349
|
+
|
|
350
|
+
def initialize(headers, url)
|
|
351
|
+
@headers = headers.transform_keys { |key| key.to_s.downcase }
|
|
352
|
+
@url = url
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def get_header(key)
|
|
356
|
+
normalized = key.to_s
|
|
357
|
+
.sub(/\AHTTP_/, "")
|
|
358
|
+
.downcase
|
|
359
|
+
.tr("_", "-")
|
|
360
|
+
headers[normalized] || headers[key.to_s] || headers[key.to_s.downcase]
|
|
361
|
+
end
|
|
362
|
+
end
|
|
210
363
|
end
|
|
211
364
|
end
|
data/lib/better_auth/cookies.rb
CHANGED
|
@@ -40,7 +40,7 @@ module BetterAuth
|
|
|
40
40
|
uri.host unless uri.host.to_s.empty?
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
|
-
raise Error, "base_url is required when cross_subdomain_cookies are enabled" if cross_subdomain && domain.to_s.empty?
|
|
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
44
|
|
|
45
45
|
custom = advanced.dig(:cookies, cookie_name.to_sym) || {}
|
|
46
46
|
prefix = advanced[:cookie_prefix] || "better-auth"
|
|
@@ -112,7 +112,9 @@ module BetterAuth
|
|
|
112
112
|
cookie = ctx.context.auth_cookies[:session_data]
|
|
113
113
|
max_age = dont_remember_me ? nil : cookie.attributes[:max_age]
|
|
114
114
|
data = filtered_cache_data(ctx, session)
|
|
115
|
-
|
|
115
|
+
strategy = config[:strategy] || "compact"
|
|
116
|
+
secret = (strategy.to_s == "jwe") ? ctx.context.secret_config : ctx.context.secret
|
|
117
|
+
value = encode_cookie_cache(data, secret, strategy: strategy, max_age: max_age || 60 * 5)
|
|
116
118
|
attributes = cookie.attributes.merge(max_age: max_age)
|
|
117
119
|
store = SessionStore.new(cookie.name, attributes, ctx)
|
|
118
120
|
|
|
@@ -129,7 +131,7 @@ module BetterAuth
|
|
|
129
131
|
|
|
130
132
|
cookie = ctx.context.auth_cookies[:account_data]
|
|
131
133
|
attributes = cookie.attributes.merge(max_age: cookie.attributes[:max_age] || 60 * 5)
|
|
132
|
-
value = Crypto.symmetric_encode_jwt(stringify_keys(account_data), ctx.context.
|
|
134
|
+
value = Crypto.symmetric_encode_jwt(stringify_keys(account_data), ctx.context.secret_config, "better-auth-account", expires_in: attributes[:max_age])
|
|
133
135
|
store = SessionStore.new(cookie.name, attributes, ctx)
|
|
134
136
|
|
|
135
137
|
if value.length > SessionStore::CHUNK_SIZE
|
|
@@ -145,7 +147,7 @@ module BetterAuth
|
|
|
145
147
|
value = SessionStore.get_chunked_cookie(ctx, cookie.name)
|
|
146
148
|
return nil unless value
|
|
147
149
|
|
|
148
|
-
Crypto.symmetric_decode_jwt(value, ctx.context.
|
|
150
|
+
Crypto.symmetric_decode_jwt(value, ctx.context.secret_config, "better-auth-account")
|
|
149
151
|
end
|
|
150
152
|
|
|
151
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)
|
data/lib/better_auth/core.rb
CHANGED
|
@@ -32,7 +32,9 @@ 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,
|
|
35
36
|
link_social: Routes.link_social,
|
|
37
|
+
link_social_account: Routes.link_social,
|
|
36
38
|
unlink_account: Routes.unlink_account,
|
|
37
39
|
get_access_token: Routes.get_access_token,
|
|
38
40
|
refresh_token: Routes.refresh_token,
|
|
@@ -26,7 +26,7 @@ module BetterAuth
|
|
|
26
26
|
"exp" => Time.now.to_i + expires_in.to_i,
|
|
27
27
|
"jti" => SecureRandom.uuid
|
|
28
28
|
)
|
|
29
|
-
key = encryption_key(secret, salt)
|
|
29
|
+
key = encryption_key(current_secret(secret), salt)
|
|
30
30
|
::JWE.encrypt(JSON.generate(claims), key, alg: ALG, enc: ENC, kid: thumbprint(key))
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -35,12 +35,17 @@ module BetterAuth
|
|
|
35
35
|
|
|
36
36
|
header = protected_header(token)
|
|
37
37
|
return nil unless valid_header?(header)
|
|
38
|
-
return nil unless header["kid"].nil? || header["kid"] == thumbprint(encryption_key(secret, salt))
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
decryption_keys(secret, salt, header["kid"]).each do |key|
|
|
40
|
+
payload = JSON.parse(::JWE.decrypt(token.to_s, key))
|
|
41
|
+
return nil if expired?(payload)
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
return payload
|
|
44
|
+
rescue JSON::ParserError, ::JWE::DecodeError, ::JWE::InvalidData, ::JWE::BadCEK
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
nil
|
|
44
49
|
rescue JSON::ParserError, ArgumentError, ::JWE::DecodeError, ::JWE::InvalidData, ::JWE::BadCEK
|
|
45
50
|
nil
|
|
46
51
|
end
|
|
@@ -57,6 +62,23 @@ module BetterAuth
|
|
|
57
62
|
Crypto.base64url_encode(OpenSSL::Digest.digest("SHA256", JSON.generate(jwk)))
|
|
58
63
|
end
|
|
59
64
|
|
|
65
|
+
def current_secret(secret)
|
|
66
|
+
secret.is_a?(SecretConfig) ? secret.current_secret : secret
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def all_secrets(secret)
|
|
70
|
+
return [[0, secret]] unless secret.is_a?(SecretConfig)
|
|
71
|
+
|
|
72
|
+
secret.all_secrets
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def decryption_keys(secret, salt, kid)
|
|
76
|
+
keys = all_secrets(secret).map { |_version, value| encryption_key(value, salt) }
|
|
77
|
+
return keys if kid.nil?
|
|
78
|
+
|
|
79
|
+
keys.select { |key| thumbprint(key) == kid }
|
|
80
|
+
end
|
|
81
|
+
|
|
60
82
|
def protected_header(token)
|
|
61
83
|
first_segment = token.to_s.split(".", 2).first
|
|
62
84
|
JSON.parse(Crypto.base64url_decode(first_segment))
|
data/lib/better_auth/crypto.rb
CHANGED
|
@@ -73,6 +73,11 @@ module BetterAuth
|
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def symmetric_encrypt(key:, data:)
|
|
76
|
+
if key.is_a?(SecretConfig)
|
|
77
|
+
ciphertext = symmetric_encrypt(key: key.current_secret, data: data)
|
|
78
|
+
return "#{SecretConfig::ENVELOPE_PREFIX}#{key.current_version}$#{ciphertext}"
|
|
79
|
+
end
|
|
80
|
+
|
|
76
81
|
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
77
82
|
cipher.encrypt
|
|
78
83
|
cipher.key = OpenSSL::Digest.digest("SHA256", key.to_s)
|
|
@@ -88,6 +93,20 @@ module BetterAuth
|
|
|
88
93
|
end
|
|
89
94
|
|
|
90
95
|
def symmetric_decrypt(key:, data:)
|
|
96
|
+
if key.is_a?(SecretConfig)
|
|
97
|
+
envelope = parse_envelope(data)
|
|
98
|
+
if envelope
|
|
99
|
+
secret = key.keys[envelope[:version]]
|
|
100
|
+
return nil unless secret
|
|
101
|
+
|
|
102
|
+
return symmetric_decrypt(key: secret, data: envelope[:ciphertext])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
return nil unless key.legacy_secret
|
|
106
|
+
|
|
107
|
+
return symmetric_decrypt(key: key.legacy_secret, data: data)
|
|
108
|
+
end
|
|
109
|
+
|
|
91
110
|
payload = JSON.parse(base64url_decode(data.to_s))
|
|
92
111
|
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
93
112
|
cipher.decrypt
|
|
@@ -137,6 +156,19 @@ module BetterAuth
|
|
|
137
156
|
value
|
|
138
157
|
end
|
|
139
158
|
|
|
159
|
+
def parse_envelope(data)
|
|
160
|
+
value = data.to_s
|
|
161
|
+
return nil unless value.start_with?(SecretConfig::ENVELOPE_PREFIX)
|
|
162
|
+
|
|
163
|
+
rest = value.delete_prefix(SecretConfig::ENVELOPE_PREFIX)
|
|
164
|
+
version, ciphertext = rest.split("$", 2)
|
|
165
|
+
return nil if version.to_s.empty? || ciphertext.to_s.empty?
|
|
166
|
+
|
|
167
|
+
{version: SecretConfig.parse_version!(version, source: "encrypted envelope"), ciphertext: ciphertext}
|
|
168
|
+
rescue Error
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
140
172
|
def keccak256_bytes(input)
|
|
141
173
|
rate = 136
|
|
142
174
|
state = Array.new(25, 0)
|
|
@@ -12,7 +12,7 @@ module BetterAuth
|
|
|
12
12
|
def create(data, model, custom: nil, context: nil)
|
|
13
13
|
run_before(model, :create, data, context) do |actual_data|
|
|
14
14
|
created = custom ? custom.call(actual_data) : adapter.create(model: model, data: actual_data, force_allow_id: true)
|
|
15
|
-
run_after(model, :create, created)
|
|
15
|
+
run_after(model, :create, created, context)
|
|
16
16
|
created
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -20,7 +20,7 @@ module BetterAuth
|
|
|
20
20
|
def update(data, where, model, custom: nil, context: nil)
|
|
21
21
|
run_before(model, :update, data, context) do |actual_data|
|
|
22
22
|
updated = custom ? custom.call(actual_data) : adapter.update(model: model, where: where, update: actual_data)
|
|
23
|
-
run_after(model, :update, updated) if updated
|
|
23
|
+
run_after(model, :update, updated, context) if updated
|
|
24
24
|
updated
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -28,16 +28,16 @@ module BetterAuth
|
|
|
28
28
|
def update_many(data, where, model, custom: nil, context: nil)
|
|
29
29
|
run_before(model, :update, data, context) do |actual_data|
|
|
30
30
|
updated = custom ? custom.call(actual_data) : adapter.update_many(model: model, where: where, update: actual_data)
|
|
31
|
-
run_after(model, :update, updated) if updated
|
|
31
|
+
run_after(model, :update, updated, context) if updated
|
|
32
32
|
updated
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def delete(where, model, custom: nil, context: nil)
|
|
37
37
|
entity = adapter.find_one(model: model, where: where)
|
|
38
|
-
return
|
|
38
|
+
return nil unless entity
|
|
39
39
|
|
|
40
|
-
return
|
|
40
|
+
return false if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
|
|
41
41
|
|
|
42
42
|
deleted = custom ? custom.call(where) : adapter.delete(model: model, where: where)
|
|
43
43
|
after_hooks(model, :delete).each { |hook| hook.call(entity, context) }
|
|
@@ -47,7 +47,7 @@ module BetterAuth
|
|
|
47
47
|
def delete_many(where, model, custom: nil, context: nil)
|
|
48
48
|
entities = adapter.find_many(model: model, where: where)
|
|
49
49
|
entities.each do |entity|
|
|
50
|
-
return
|
|
50
|
+
return false if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
|
|
51
51
|
end
|
|
52
52
|
deleted = custom ? custom.call(where) : adapter.delete_many(model: model, where: where)
|
|
53
53
|
entities.each { |entity| after_hooks(model, :delete).each { |hook| hook.call(entity, context) } }
|
|
@@ -68,8 +68,8 @@ module BetterAuth
|
|
|
68
68
|
yield actual_data
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
def run_after(model, action, data)
|
|
72
|
-
after_hooks(model, action).each { |hook| hook.call(data,
|
|
71
|
+
def run_after(model, action, data, context)
|
|
72
|
+
after_hooks(model, action).each { |hook| hook.call(data, context) }
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def before_hooks(model, action)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Deprecate
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def wrap(message, logger: nil, &block)
|
|
8
|
+
warned = false
|
|
9
|
+
proc do |*args, **kwargs|
|
|
10
|
+
unless warned
|
|
11
|
+
warn_once("[Deprecation] #{message}", logger)
|
|
12
|
+
warned = true
|
|
13
|
+
end
|
|
14
|
+
kwargs.empty? ? block.call(*args) : block.call(*args, **kwargs)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def warn_once(message, logger)
|
|
19
|
+
if logger.respond_to?(:call)
|
|
20
|
+
logger.call(message)
|
|
21
|
+
elsif logger.respond_to?(:warn)
|
|
22
|
+
logger.warn(message)
|
|
23
|
+
else
|
|
24
|
+
warn(message)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|