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
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
|
|
@@ -168,6 +212,7 @@ module BetterAuth
|
|
|
168
212
|
|
|
169
213
|
payload = decode_cookie_cache(raw, secret, strategy: strategy)
|
|
170
214
|
return nil unless payload && payload["session"] && payload["user"]
|
|
215
|
+
return nil if embedded_session_expired?(payload["session"])
|
|
171
216
|
|
|
172
217
|
expected_version = cookie_cache_version(version, payload["session"], payload["user"])
|
|
173
218
|
return nil if version && (payload["version"] || "1") != expected_version
|
|
@@ -176,6 +221,7 @@ module BetterAuth
|
|
|
176
221
|
end
|
|
177
222
|
|
|
178
223
|
def expire_cookie(ctx, cookie)
|
|
224
|
+
remove_set_cookie_entries(ctx, cookie.name)
|
|
179
225
|
ctx.set_cookie(cookie.name, "", cookie.attributes.merge(max_age: 0))
|
|
180
226
|
end
|
|
181
227
|
|
|
@@ -184,6 +230,11 @@ module BetterAuth
|
|
|
184
230
|
expire_cookie(ctx, ctx.context.auth_cookies[:session_data])
|
|
185
231
|
expire_cookie(ctx, ctx.context.auth_cookies[:account_data]) if ctx.context.options.account[:store_account_cookie]
|
|
186
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
|
+
|
|
187
238
|
store = SessionStore.new(ctx.context.auth_cookies[:session_data].name, ctx.context.auth_cookies[:session_data].attributes, ctx)
|
|
188
239
|
store.set_cookies(store.clean)
|
|
189
240
|
expire_cookie(ctx, ctx.context.auth_cookies[:dont_remember]) unless skip_dont_remember_me
|
|
@@ -250,12 +301,57 @@ module BetterAuth
|
|
|
250
301
|
chunks.sort_by(&:first).map(&:last).join
|
|
251
302
|
end
|
|
252
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
|
+
|
|
253
328
|
def decode_cookie_value(value)
|
|
254
|
-
URI.decode_uri_component(value)
|
|
329
|
+
decoded = URI.decode_uri_component(value)
|
|
330
|
+
decoded.valid_encoding? ? decoded : value
|
|
255
331
|
rescue ArgumentError
|
|
256
332
|
value
|
|
257
333
|
end
|
|
258
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
|
+
|
|
259
355
|
def header_value(request_or_cookie_header)
|
|
260
356
|
return request_or_cookie_header.headers["cookie"] if request_or_cookie_header.respond_to?(:headers)
|
|
261
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
|
data/lib/better_auth/doctor.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "better_auth/sql_migration"
|
|
|
4
4
|
|
|
5
5
|
module BetterAuth
|
|
6
6
|
module Doctor
|
|
7
|
-
Result = Struct.new(:ok, :warnings, :errors
|
|
7
|
+
Result = Struct.new(:ok, :warnings, :errors) do
|
|
8
8
|
def success?
|
|
9
9
|
errors.empty?
|
|
10
10
|
end
|
|
@@ -31,6 +31,15 @@ module BetterAuth
|
|
|
31
31
|
result.success? ? 0 : 1
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def as_json(result)
|
|
35
|
+
{
|
|
36
|
+
"ok" => result.ok,
|
|
37
|
+
"warnings" => result.warnings,
|
|
38
|
+
"errors" => result.errors,
|
|
39
|
+
"success" => result.success?
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
34
43
|
def check_secret(config, result)
|
|
35
44
|
secret = config.secret.to_s
|
|
36
45
|
if secret.empty?
|
|
@@ -49,7 +58,7 @@ module BetterAuth
|
|
|
49
58
|
def check_base_url(config, result)
|
|
50
59
|
base_url = config.base_url.to_s
|
|
51
60
|
if base_url.empty?
|
|
52
|
-
result.
|
|
61
|
+
result.errors << "base_url is not configured"
|
|
53
62
|
elsif !base_url.start_with?("https://")
|
|
54
63
|
result.warnings << "base_url is not HTTPS"
|
|
55
64
|
else
|
|
@@ -71,7 +80,9 @@ module BetterAuth
|
|
|
71
80
|
auth = BetterAuth.auth(config.to_h)
|
|
72
81
|
adapter = auth.context.adapter
|
|
73
82
|
unless adapter.respond_to?(:dialect) && adapter.respond_to?(:connection)
|
|
74
|
-
|
|
83
|
+
warning = "database adapter does not expose SQL migration introspection; schema drift check skipped"
|
|
84
|
+
warning += "; run `better-auth mongo indexes` to ensure MongoDB indexes" if adapter.respond_to?(:ensure_indexes!)
|
|
85
|
+
result.warnings << warning
|
|
75
86
|
return
|
|
76
87
|
end
|
|
77
88
|
|
data/lib/better_auth/error.rb
CHANGED
|
@@ -17,6 +17,7 @@ module BetterAuth
|
|
|
17
17
|
"PROVIDER_NOT_FOUND" => "Provider not found",
|
|
18
18
|
"INVALID_TOKEN" => "Invalid token",
|
|
19
19
|
"TOKEN_EXPIRED" => "Token expired",
|
|
20
|
+
"TOKEN_ALREADY_USED" => "Token has already been used",
|
|
20
21
|
"ID_TOKEN_NOT_SUPPORTED" => "id_token not supported",
|
|
21
22
|
"FAILED_TO_GET_USER_INFO" => "Failed to get user info",
|
|
22
23
|
"USER_EMAIL_NOT_FOUND" => "User email not found",
|
|
@@ -54,6 +55,7 @@ module BetterAuth
|
|
|
54
55
|
"PASSWORD_ALREADY_SET" => "User already has a password set",
|
|
55
56
|
"RESET_PASSWORD_DISABLED" => "Reset password isn't enabled",
|
|
56
57
|
"EMAIL_PASSWORD_DISABLED" => "Email and password is not enabled",
|
|
57
|
-
"EMAIL_PASSWORD_SIGN_UP_DISABLED" => "Email and password sign up is not enabled"
|
|
58
|
+
"EMAIL_PASSWORD_SIGN_UP_DISABLED" => "Email and password sign up is not enabled",
|
|
59
|
+
"CHANGE_EMAIL_DISABLED" => "Change email is disabled"
|
|
58
60
|
}.freeze
|
|
59
61
|
end
|
data/lib/better_auth/logger.rb
CHANGED
|
@@ -4,7 +4,7 @@ module BetterAuth
|
|
|
4
4
|
module Logger
|
|
5
5
|
LEVELS = [:debug, :info, :success, :warn, :error].freeze
|
|
6
6
|
|
|
7
|
-
Internal = Struct.new(:level, :disabled, :handler
|
|
7
|
+
Internal = Struct.new(:level, :disabled, :handler) do
|
|
8
8
|
LEVELS.each do |log_level|
|
|
9
9
|
define_method(log_level) do |message, *args|
|
|
10
10
|
return if disabled || !Logger.should_publish?(level, log_level)
|
|
@@ -55,7 +55,11 @@ module BetterAuth
|
|
|
55
55
|
mode = headers["sec-fetch-mode"]
|
|
56
56
|
dest = headers["sec-fetch-dest"]
|
|
57
57
|
has_metadata = [site, mode, dest].any? { |value| value && !value.to_s.strip.empty? }
|
|
58
|
-
|
|
58
|
+
unless has_metadata
|
|
59
|
+
origin = headers["origin"] || headers["referer"]
|
|
60
|
+
validate_origin(endpoint_context, force: true) if login_or_signup_path?(endpoint_context.path) && origin && !origin.to_s.empty?
|
|
61
|
+
return
|
|
62
|
+
end
|
|
59
63
|
|
|
60
64
|
if site == "cross-site" && mode == "navigate"
|
|
61
65
|
log(endpoint_context.context, :error, "Blocked cross-site navigation login attempt (CSRF protection)")
|
|
@@ -109,6 +113,10 @@ module BetterAuth
|
|
|
109
113
|
skip.any? { |path| endpoint_context.path.start_with?(path.to_s) }
|
|
110
114
|
end
|
|
111
115
|
|
|
116
|
+
def login_or_signup_path?(path)
|
|
117
|
+
path.to_s.match?(%r{/(?:sign-in|sign-up)(?:/|$)})
|
|
118
|
+
end
|
|
119
|
+
|
|
112
120
|
def fetch_data(data, key)
|
|
113
121
|
return unless data.is_a?(Hash)
|
|
114
122
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module BetterAuth
|
|
4
4
|
module MigrationPlan
|
|
5
|
-
TableChange = Struct.new(:logical_name, :table_name, :table, :order
|
|
6
|
-
FieldChange = Struct.new(:logical_name, :table_name, :fields, :table, :order
|
|
7
|
-
IndexChange = Struct.new(:table_name, :field_name, :name, :unique, :field
|
|
5
|
+
TableChange = Struct.new(:logical_name, :table_name, :table, :order)
|
|
6
|
+
FieldChange = Struct.new(:logical_name, :table_name, :fields, :table, :order)
|
|
7
|
+
IndexChange = Struct.new(:table_name, :field_name, :name, :unique, :field)
|
|
8
8
|
|
|
9
|
-
Plan = Struct.new(:to_create, :to_add, :to_index, :warnings, :dialect, :tables
|
|
9
|
+
Plan = Struct.new(:to_create, :to_add, :to_index, :warnings, :dialect, :tables) do
|
|
10
10
|
def empty?
|
|
11
11
|
to_create.empty? && to_add.empty? && to_index.empty?
|
|
12
12
|
end
|