better_auth 0.4.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 +2 -0
- data/README.md +24 -0
- data/lib/better_auth/adapters/internal_adapter.rb +5 -5
- data/lib/better_auth/adapters/sql.rb +96 -18
- data/lib/better_auth/api.rb +113 -13
- 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 +5 -5
- data/lib/better_auth/endpoint.rb +87 -3
- data/lib/better_auth/error.rb +8 -1
- 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 +246 -15
- data/lib/better_auth/plugins/expo.rb +17 -1
- data/lib/better_auth/plugins/generic_oauth.rb +53 -7
- 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 +2 -2
- 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.rb +135 -36
- 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 +65 -23
- data/lib/better_auth/plugins/username.rb +57 -2
- data/lib/better_auth/rate_limiter.rb +20 -0
- data/lib/better_auth/response.rb +42 -0
- data/lib/better_auth/router.rb +7 -1
- data/lib/better_auth/routes/account.rb +204 -38
- data/lib/better_auth/routes/email_verification.rb +98 -14
- data/lib/better_auth/routes/password.rb +125 -8
- data/lib/better_auth/routes/session.rb +128 -13
- data/lib/better_auth/routes/sign_in.rb +24 -2
- data/lib/better_auth/routes/sign_out.rb +13 -1
- data/lib/better_auth/routes/sign_up.rb +62 -4
- data/lib/better_auth/routes/social.rb +102 -7
- data/lib/better_auth/routes/user.rb +222 -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 +1 -1
- data/lib/better_auth/url_helpers.rb +12 -1
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +4 -0
- metadata +15 -1
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,7 +28,7 @@ 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
|
|
@@ -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)
|
data/lib/better_auth/endpoint.rb
CHANGED
|
@@ -10,6 +10,7 @@ module BetterAuth
|
|
|
10
10
|
:params_schema,
|
|
11
11
|
:headers_schema,
|
|
12
12
|
:metadata,
|
|
13
|
+
:options,
|
|
13
14
|
:use,
|
|
14
15
|
:handler
|
|
15
16
|
|
|
@@ -21,6 +22,9 @@ module BetterAuth
|
|
|
21
22
|
@params_schema = params_schema
|
|
22
23
|
@headers_schema = headers_schema
|
|
23
24
|
@metadata = metadata || {}
|
|
25
|
+
apply_default_open_api_metadata!
|
|
26
|
+
apply_open_api_schemas!
|
|
27
|
+
@options = endpoint_options
|
|
24
28
|
@use = Array(use)
|
|
25
29
|
@handler = handler || ->(_ctx) {}
|
|
26
30
|
end
|
|
@@ -46,6 +50,34 @@ module BetterAuth
|
|
|
46
50
|
|
|
47
51
|
private
|
|
48
52
|
|
|
53
|
+
def endpoint_options
|
|
54
|
+
{
|
|
55
|
+
method: (methods.length == 1) ? methods.first : methods,
|
|
56
|
+
body: body_schema,
|
|
57
|
+
query: query_schema,
|
|
58
|
+
params: params_schema,
|
|
59
|
+
headers: headers_schema,
|
|
60
|
+
metadata: metadata
|
|
61
|
+
}.compact
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def apply_default_open_api_metadata!
|
|
65
|
+
return unless path
|
|
66
|
+
return if metadata[:openapi] || metadata[:hide] || metadata[:SERVER_ONLY] || metadata[:server_only]
|
|
67
|
+
return unless defined?(BetterAuth::OpenAPI)
|
|
68
|
+
|
|
69
|
+
metadata[:openapi] = BetterAuth::OpenAPI.default_metadata(path, methods)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def apply_open_api_schemas!
|
|
73
|
+
openapi = fetch_key(metadata, :openapi)
|
|
74
|
+
return unless openapi.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
@body_schema ||= schema_for_open_api_request_body(openapi)
|
|
77
|
+
@query_schema ||= schema_for_open_api_parameters(openapi, "query")
|
|
78
|
+
@headers_schema ||= schema_for_open_api_parameters(openapi, "header")
|
|
79
|
+
end
|
|
80
|
+
|
|
49
81
|
def apply_schemas!(context)
|
|
50
82
|
context.body = validate_schema(:body, body_schema, context.body)
|
|
51
83
|
context.query = validate_schema(:query, query_schema, context.query)
|
|
@@ -86,6 +118,54 @@ module BetterAuth
|
|
|
86
118
|
result
|
|
87
119
|
end
|
|
88
120
|
|
|
121
|
+
def schema_for_open_api_request_body(openapi)
|
|
122
|
+
schema = fetch_key(fetch_key(fetch_key(fetch_key(openapi, :requestBody), :content), "application/json"), :schema)
|
|
123
|
+
required = Array(fetch_key(schema, :required)).map(&:to_s)
|
|
124
|
+
return nil if required.empty?
|
|
125
|
+
|
|
126
|
+
->(value) { validate_required_open_api_fields(value, required) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def schema_for_open_api_parameters(openapi, location)
|
|
130
|
+
required = Array(fetch_key(openapi, :parameters))
|
|
131
|
+
.select { |parameter| parameter.is_a?(Hash) && fetch_key(parameter, :in).to_s == location && fetch_key(parameter, :required) == true }
|
|
132
|
+
.filter_map { |parameter| fetch_key(parameter, :name) }
|
|
133
|
+
.map(&:to_s)
|
|
134
|
+
return nil if required.empty?
|
|
135
|
+
|
|
136
|
+
->(value) { validate_required_open_api_fields(value, required) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_required_open_api_fields(value, required)
|
|
140
|
+
data = normalize_open_api_input(value)
|
|
141
|
+
return false unless required.all? { |key| data.key?(open_api_storage_key(key)) && !data[open_api_storage_key(key)].nil? }
|
|
142
|
+
|
|
143
|
+
value
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def normalize_open_api_input(value)
|
|
147
|
+
return {} unless value.is_a?(Hash)
|
|
148
|
+
|
|
149
|
+
value.each_with_object({}) do |(key, object_value), result|
|
|
150
|
+
result[open_api_storage_key(key)] = object_value
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def open_api_storage_key(key)
|
|
155
|
+
key.to_s
|
|
156
|
+
.gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
|
|
157
|
+
.tr("-", "_")
|
|
158
|
+
.downcase
|
|
159
|
+
.split("_")
|
|
160
|
+
.then { |parts| ([parts.first] + parts.drop(1).map(&:capitalize)).join }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def fetch_key(hash, key)
|
|
164
|
+
return nil unless hash.respond_to?(:[])
|
|
165
|
+
|
|
166
|
+
hash[key] || hash[key.to_s]
|
|
167
|
+
end
|
|
168
|
+
|
|
89
169
|
class Result
|
|
90
170
|
attr_accessor :response, :status, :headers
|
|
91
171
|
|
|
@@ -100,7 +180,7 @@ module BetterAuth
|
|
|
100
180
|
return value if value.is_a?(self)
|
|
101
181
|
|
|
102
182
|
if value.is_a?(APIError)
|
|
103
|
-
return new(response: value, status: value.status_code, headers: value.headers)
|
|
183
|
+
return new(response: value, status: value.status_code, headers: merge_headers(context.response_headers, value.headers))
|
|
104
184
|
end
|
|
105
185
|
|
|
106
186
|
if rack_response?(value)
|
|
@@ -136,7 +216,11 @@ module BetterAuth
|
|
|
136
216
|
end
|
|
137
217
|
|
|
138
218
|
def to_rack_response
|
|
139
|
-
|
|
219
|
+
to_response.to_a
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def to_response
|
|
223
|
+
return Response.from_rack(@raw_response) if raw_response?
|
|
140
224
|
|
|
141
225
|
body = if response.nil?
|
|
142
226
|
[JSON.generate(nil)]
|
|
@@ -146,7 +230,7 @@ module BetterAuth
|
|
|
146
230
|
[JSON.generate(response)]
|
|
147
231
|
end
|
|
148
232
|
response_headers = {"content-type" => "application/json"}.merge(headers)
|
|
149
|
-
|
|
233
|
+
Response.new(status: status, headers: response_headers, body: body)
|
|
150
234
|
end
|
|
151
235
|
|
|
152
236
|
private
|
data/lib/better_auth/error.rb
CHANGED
|
@@ -16,6 +16,7 @@ module BetterAuth
|
|
|
16
16
|
"SOCIAL_ACCOUNT_ALREADY_LINKED" => "Social account already linked",
|
|
17
17
|
"PROVIDER_NOT_FOUND" => "Provider not found",
|
|
18
18
|
"INVALID_TOKEN" => "Invalid token",
|
|
19
|
+
"TOKEN_EXPIRED" => "Token expired",
|
|
19
20
|
"ID_TOKEN_NOT_SUPPORTED" => "id_token not supported",
|
|
20
21
|
"FAILED_TO_GET_USER_INFO" => "Failed to get user info",
|
|
21
22
|
"USER_EMAIL_NOT_FOUND" => "User email not found",
|
|
@@ -47,6 +48,12 @@ module BetterAuth
|
|
|
47
48
|
"FIELD_NOT_ALLOWED" => "Field not allowed to be set",
|
|
48
49
|
"ASYNC_VALIDATION_NOT_SUPPORTED" => "Async validation is not supported",
|
|
49
50
|
"VALIDATION_ERROR" => "Validation Error",
|
|
50
|
-
"MISSING_FIELD" => "Field is required"
|
|
51
|
+
"MISSING_FIELD" => "Field is required",
|
|
52
|
+
"BODY_MUST_BE_AN_OBJECT" => "Body must be an object",
|
|
53
|
+
"METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED" => "POST method requires deferSessionRefresh to be enabled in session config",
|
|
54
|
+
"PASSWORD_ALREADY_SET" => "User already has a password set",
|
|
55
|
+
"RESET_PASSWORD_DISABLED" => "Reset password isn't enabled",
|
|
56
|
+
"EMAIL_PASSWORD_DISABLED" => "Email and password is not enabled",
|
|
57
|
+
"EMAIL_PASSWORD_SIGN_UP_DISABLED" => "Email and password sign up is not enabled"
|
|
51
58
|
}.freeze
|
|
52
59
|
end
|
|
@@ -11,8 +11,8 @@ module BetterAuth
|
|
|
11
11
|
fields: {
|
|
12
12
|
role: {type: "string", required: false, input: false},
|
|
13
13
|
banned: {type: "boolean", required: false, input: false, default_value: false},
|
|
14
|
-
banReason: {type: "string", required: false, input: false},
|
|
15
|
-
banExpires: {type: "date", required: false, input: false}
|
|
14
|
+
banReason: {type: "string", required: false, input: false, default_value: nil},
|
|
15
|
+
banExpires: {type: "date", required: false, input: false, default_value: nil}
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
session: {
|