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
|
@@ -24,7 +24,37 @@ module BetterAuth
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def get_siwe_nonce_endpoint(config)
|
|
27
|
-
Endpoint.new(
|
|
27
|
+
Endpoint.new(
|
|
28
|
+
path: "/siwe/nonce",
|
|
29
|
+
method: "POST",
|
|
30
|
+
body_schema: ->(body) { siwe_nonce_body(body) },
|
|
31
|
+
metadata: {
|
|
32
|
+
openapi: {
|
|
33
|
+
operationId: "getSiweNonce",
|
|
34
|
+
description: "Generate a nonce for Sign-In with Ethereum",
|
|
35
|
+
requestBody: OpenAPI.json_request_body(
|
|
36
|
+
OpenAPI.object_schema(
|
|
37
|
+
{
|
|
38
|
+
walletAddress: {type: "string"},
|
|
39
|
+
chainId: {type: ["number", "string", "null"]}
|
|
40
|
+
},
|
|
41
|
+
required: ["walletAddress"]
|
|
42
|
+
)
|
|
43
|
+
),
|
|
44
|
+
responses: {
|
|
45
|
+
"200" => OpenAPI.json_response(
|
|
46
|
+
"SIWE nonce",
|
|
47
|
+
OpenAPI.object_schema(
|
|
48
|
+
{
|
|
49
|
+
nonce: {type: "string"}
|
|
50
|
+
},
|
|
51
|
+
required: ["nonce"]
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
) do |ctx|
|
|
28
58
|
body = normalize_hash(ctx.body)
|
|
29
59
|
wallet_address = siwe_normalize_wallet!(body[:wallet_address])
|
|
30
60
|
chain_id = siwe_chain_id(body[:chain_id])
|
|
@@ -42,7 +72,42 @@ module BetterAuth
|
|
|
42
72
|
end
|
|
43
73
|
|
|
44
74
|
def verify_siwe_message_endpoint(config)
|
|
45
|
-
Endpoint.new(
|
|
75
|
+
Endpoint.new(
|
|
76
|
+
path: "/siwe/verify",
|
|
77
|
+
method: "POST",
|
|
78
|
+
body_schema: ->(body) { siwe_verify_body(body, config) },
|
|
79
|
+
metadata: {
|
|
80
|
+
openapi: {
|
|
81
|
+
operationId: "verifySiweMessage",
|
|
82
|
+
description: "Verify a Sign-In with Ethereum message",
|
|
83
|
+
requestBody: OpenAPI.json_request_body(
|
|
84
|
+
OpenAPI.object_schema(
|
|
85
|
+
{
|
|
86
|
+
walletAddress: {type: "string"},
|
|
87
|
+
chainId: {type: ["number", "string", "null"]},
|
|
88
|
+
message: {type: "string"},
|
|
89
|
+
signature: {type: "string"},
|
|
90
|
+
email: {type: ["string", "null"]}
|
|
91
|
+
},
|
|
92
|
+
required: ["walletAddress", "message", "signature"]
|
|
93
|
+
)
|
|
94
|
+
),
|
|
95
|
+
responses: {
|
|
96
|
+
"200" => OpenAPI.json_response(
|
|
97
|
+
"SIWE message verified",
|
|
98
|
+
OpenAPI.object_schema(
|
|
99
|
+
{
|
|
100
|
+
token: {type: "string"},
|
|
101
|
+
success: {type: "boolean"},
|
|
102
|
+
user: {type: "object"}
|
|
103
|
+
},
|
|
104
|
+
required: ["token", "success", "user"]
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
) do |ctx|
|
|
46
111
|
body = normalize_hash(ctx.body)
|
|
47
112
|
wallet_address = siwe_normalize_wallet!(body[:wallet_address])
|
|
48
113
|
chain_id = siwe_chain_id(body[:chain_id])
|
|
@@ -203,7 +268,8 @@ module BetterAuth
|
|
|
203
268
|
ctx.context.internal_adapter.create_user(
|
|
204
269
|
name: ens[:name] || wallet_address,
|
|
205
270
|
email: (anonymous == false && !email.empty?) ? email : "#{wallet_address}@#{domain}",
|
|
206
|
-
image: ens[:avatar] || ""
|
|
271
|
+
image: ens[:avatar] || "",
|
|
272
|
+
context: ctx
|
|
207
273
|
)
|
|
208
274
|
end
|
|
209
275
|
|
|
@@ -79,13 +79,13 @@ module BetterAuth
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def two_factor_enable_endpoint(config)
|
|
82
|
-
Endpoint.new(path: "/two-factor/enable", method: "POST") do |ctx|
|
|
83
|
-
session = Routes.current_session(ctx
|
|
82
|
+
Endpoint.new(path: "/two-factor/enable", method: "POST", metadata: two_factor_openapi("enableTwoFactor", "Enable two factor authentication", two_factor_enable_response_schema)) do |ctx|
|
|
83
|
+
session = Routes.current_session(ctx)
|
|
84
84
|
body = normalize_hash(ctx.body)
|
|
85
85
|
two_factor_check_password!(ctx, session[:user]["id"], body[:password], allow_passwordless: config[:allow_passwordless])
|
|
86
86
|
|
|
87
87
|
secret = two_factor_generate_secret
|
|
88
|
-
backup = two_factor_generate_backup_codes(ctx.context.
|
|
88
|
+
backup = two_factor_generate_backup_codes(ctx.context.secret_config, config[:backup_code_options])
|
|
89
89
|
if config[:skip_verification_on_enable]
|
|
90
90
|
updated_user = ctx.context.internal_adapter.update_user(session[:user]["id"], twoFactorEnabled: true)
|
|
91
91
|
new_session = ctx.context.internal_adapter.create_session(updated_user["id"], false)
|
|
@@ -99,7 +99,7 @@ module BetterAuth
|
|
|
99
99
|
ctx.context.adapter.create(
|
|
100
100
|
model: TWO_FACTOR_MODEL,
|
|
101
101
|
data: {
|
|
102
|
-
secret: Crypto.symmetric_encrypt(key: ctx.context.
|
|
102
|
+
secret: Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: secret),
|
|
103
103
|
backupCodes: backup[:stored],
|
|
104
104
|
userId: session[:user]["id"],
|
|
105
105
|
verified: verified
|
|
@@ -115,8 +115,8 @@ module BetterAuth
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def two_factor_disable_endpoint(config)
|
|
118
|
-
Endpoint.new(path: "/two-factor/disable", method: "POST") do |ctx|
|
|
119
|
-
session = Routes.current_session(ctx
|
|
118
|
+
Endpoint.new(path: "/two-factor/disable", method: "POST", metadata: two_factor_openapi("disableTwoFactor", "Disable two factor authentication", OpenAPI.status_response_schema)) do |ctx|
|
|
119
|
+
session = Routes.current_session(ctx)
|
|
120
120
|
body = normalize_hash(ctx.body)
|
|
121
121
|
two_factor_check_password!(ctx, session[:user]["id"], body[:password], allow_passwordless: config[:allow_passwordless])
|
|
122
122
|
|
|
@@ -138,7 +138,7 @@ module BetterAuth
|
|
|
138
138
|
end
|
|
139
139
|
|
|
140
140
|
def two_factor_generate_totp_endpoint(config)
|
|
141
|
-
Endpoint.new(path: "/totp/generate", method: "POST") do |ctx|
|
|
141
|
+
Endpoint.new(path: "/totp/generate", method: "POST", metadata: two_factor_openapi("generateTOTP", "Generate a TOTP code", OpenAPI.object_schema({code: {type: "string"}}, required: ["code"]))) do |ctx|
|
|
142
142
|
two_factor_totp_enabled!(config)
|
|
143
143
|
body = normalize_hash(ctx.body)
|
|
144
144
|
ctx.json({code: two_factor_totp(body[:secret], options: config[:totp_options])})
|
|
@@ -146,20 +146,20 @@ module BetterAuth
|
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
def two_factor_get_totp_uri_endpoint(config)
|
|
149
|
-
Endpoint.new(path: "/two-factor/get-totp-uri", method: "POST") do |ctx|
|
|
149
|
+
Endpoint.new(path: "/two-factor/get-totp-uri", method: "POST", metadata: two_factor_openapi("getTOTPURI", "Get the TOTP URI", OpenAPI.object_schema({totpURI: {type: "string"}}, required: ["totpURI"]))) do |ctx|
|
|
150
150
|
two_factor_totp_enabled!(config)
|
|
151
|
-
session = Routes.current_session(ctx
|
|
151
|
+
session = Routes.current_session(ctx)
|
|
152
152
|
two_factor_check_password!(ctx, session[:user]["id"], normalize_hash(ctx.body)[:password], allow_passwordless: config[:totp_options][:allow_passwordless])
|
|
153
153
|
record = two_factor_record(ctx, config, session[:user]["id"])
|
|
154
154
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TOTP_NOT_ENABLED"]) unless record
|
|
155
155
|
|
|
156
|
-
secret = Crypto.symmetric_decrypt(key: ctx.context.
|
|
156
|
+
secret = Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: record["secret"])
|
|
157
157
|
ctx.json({totpURI: two_factor_totp_uri(secret, issuer: config[:issuer] || ctx.context.app_name, account: session[:user]["email"], options: config[:totp_options])})
|
|
158
158
|
end
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
def two_factor_verify_totp_endpoint(config)
|
|
162
|
-
Endpoint.new(path: "/two-factor/verify-totp", method: "POST") do |ctx|
|
|
162
|
+
Endpoint.new(path: "/two-factor/verify-totp", method: "POST", metadata: two_factor_openapi("verifyTOTP", "Verify a TOTP code", two_factor_verification_response_schema)) do |ctx|
|
|
163
163
|
two_factor_totp_enabled!(config)
|
|
164
164
|
body = normalize_hash(ctx.body)
|
|
165
165
|
data = two_factor_verification_context(ctx, config)
|
|
@@ -169,7 +169,7 @@ module BetterAuth
|
|
|
169
169
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TOTP_NOT_ENABLED"])
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
secret = Crypto.symmetric_decrypt(key: ctx.context.
|
|
172
|
+
secret = Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: record["secret"])
|
|
173
173
|
raise APIError.new("UNAUTHORIZED", message: TWO_FACTOR_ERROR_CODES["INVALID_CODE"]) unless two_factor_totp_valid?(secret, body[:code], options: config[:totp_options])
|
|
174
174
|
|
|
175
175
|
if record["verified"] != true
|
|
@@ -191,7 +191,7 @@ module BetterAuth
|
|
|
191
191
|
end
|
|
192
192
|
|
|
193
193
|
def two_factor_send_otp_endpoint(config)
|
|
194
|
-
Endpoint.new(path: "/two-factor/send-otp", method: "POST") do |ctx|
|
|
194
|
+
Endpoint.new(path: "/two-factor/send-otp", method: "POST", metadata: two_factor_openapi("sendTwoFactorOTP", "Send a two factor OTP", OpenAPI.status_response_schema)) do |ctx|
|
|
195
195
|
otp_config = config[:otp_options]
|
|
196
196
|
sender = otp_config[:send_otp]
|
|
197
197
|
unless sender.respond_to?(:call)
|
|
@@ -212,7 +212,7 @@ module BetterAuth
|
|
|
212
212
|
end
|
|
213
213
|
|
|
214
214
|
def two_factor_verify_otp_endpoint(config)
|
|
215
|
-
Endpoint.new(path: "/two-factor/verify-otp", method: "POST") do |ctx|
|
|
215
|
+
Endpoint.new(path: "/two-factor/verify-otp", method: "POST", metadata: two_factor_openapi("verifyTwoFactorOTP", "Verify a two factor OTP", two_factor_verification_response_schema)) do |ctx|
|
|
216
216
|
body = normalize_hash(ctx.body)
|
|
217
217
|
data = two_factor_verification_context(ctx, config)
|
|
218
218
|
verification = ctx.context.internal_adapter.find_verification_value("2fa-otp-#{data[:key]}")
|
|
@@ -246,19 +246,19 @@ module BetterAuth
|
|
|
246
246
|
end
|
|
247
247
|
|
|
248
248
|
def two_factor_verify_backup_code_endpoint(config)
|
|
249
|
-
Endpoint.new(path: "/two-factor/verify-backup-code", method: "POST") do |ctx|
|
|
249
|
+
Endpoint.new(path: "/two-factor/verify-backup-code", method: "POST", metadata: two_factor_openapi("verifyBackupCode", "Verify a two factor backup code", two_factor_verification_response_schema)) do |ctx|
|
|
250
250
|
body = normalize_hash(ctx.body)
|
|
251
251
|
data = two_factor_verification_context(ctx, config)
|
|
252
252
|
record = two_factor_record(ctx, config, data[:session][:user]["id"])
|
|
253
253
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["BACKUP_CODES_NOT_ENABLED"]) unless record
|
|
254
254
|
|
|
255
|
-
codes = two_factor_read_backup_codes(ctx.context.
|
|
255
|
+
codes = two_factor_read_backup_codes(ctx.context.secret_config, record["backupCodes"], config[:backup_code_options])
|
|
256
256
|
unless codes.include?(body[:code].to_s)
|
|
257
257
|
raise APIError.new("UNAUTHORIZED", message: TWO_FACTOR_ERROR_CODES["INVALID_BACKUP_CODE"])
|
|
258
258
|
end
|
|
259
259
|
|
|
260
260
|
remaining = codes.reject { |code| code == body[:code].to_s }
|
|
261
|
-
stored = two_factor_store_backup_codes(ctx.context.
|
|
261
|
+
stored = two_factor_store_backup_codes(ctx.context.secret_config, remaining, config[:backup_code_options])
|
|
262
262
|
updated = ctx.context.adapter.update(
|
|
263
263
|
model: TWO_FACTOR_MODEL,
|
|
264
264
|
where: [{field: "id", value: record["id"]}, {field: "backupCodes", value: record["backupCodes"]}],
|
|
@@ -271,15 +271,15 @@ module BetterAuth
|
|
|
271
271
|
end
|
|
272
272
|
|
|
273
273
|
def two_factor_generate_backup_codes_endpoint(config)
|
|
274
|
-
Endpoint.new(path: "/two-factor/generate-backup-codes", method: "POST") do |ctx|
|
|
275
|
-
session = Routes.current_session(ctx
|
|
274
|
+
Endpoint.new(path: "/two-factor/generate-backup-codes", method: "POST", metadata: two_factor_openapi("generateBackupCodes", "Generate two factor backup codes", two_factor_backup_codes_response_schema)) do |ctx|
|
|
275
|
+
session = Routes.current_session(ctx)
|
|
276
276
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TWO_FACTOR_NOT_ENABLED"]) unless session[:user]["twoFactorEnabled"]
|
|
277
277
|
|
|
278
278
|
two_factor_check_password!(ctx, session[:user]["id"], normalize_hash(ctx.body)[:password], allow_passwordless: config[:backup_code_options][:allow_passwordless])
|
|
279
279
|
record = two_factor_record(ctx, config, session[:user]["id"])
|
|
280
280
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TWO_FACTOR_NOT_ENABLED"]) unless record
|
|
281
281
|
|
|
282
|
-
backup = two_factor_generate_backup_codes(ctx.context.
|
|
282
|
+
backup = two_factor_generate_backup_codes(ctx.context.secret_config, config[:backup_code_options])
|
|
283
283
|
ctx.context.adapter.update(model: TWO_FACTOR_MODEL, where: [{field: "id", value: record["id"]}], update: {backupCodes: backup[:stored]})
|
|
284
284
|
ctx.json({status: true, backupCodes: backup[:codes]})
|
|
285
285
|
end
|
|
@@ -291,10 +291,52 @@ module BetterAuth
|
|
|
291
291
|
record = two_factor_record(ctx, config, body[:user_id])
|
|
292
292
|
raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["BACKUP_CODES_NOT_ENABLED"]) unless record
|
|
293
293
|
|
|
294
|
-
ctx.json({status: true, backupCodes: two_factor_read_backup_codes(ctx.context.
|
|
294
|
+
ctx.json({status: true, backupCodes: two_factor_read_backup_codes(ctx.context.secret_config, record["backupCodes"], config[:backup_code_options])})
|
|
295
295
|
end
|
|
296
296
|
end
|
|
297
297
|
|
|
298
|
+
def two_factor_openapi(operation_id, description, response_schema)
|
|
299
|
+
{
|
|
300
|
+
openapi: {
|
|
301
|
+
operationId: operation_id,
|
|
302
|
+
description: description,
|
|
303
|
+
responses: {
|
|
304
|
+
"200" => OpenAPI.json_response("Success", response_schema)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def two_factor_enable_response_schema
|
|
311
|
+
OpenAPI.object_schema(
|
|
312
|
+
{
|
|
313
|
+
totpURI: {type: "string"},
|
|
314
|
+
backupCodes: {type: "array", items: {type: "string"}}
|
|
315
|
+
},
|
|
316
|
+
required: ["totpURI", "backupCodes"]
|
|
317
|
+
)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def two_factor_verification_response_schema
|
|
321
|
+
OpenAPI.object_schema(
|
|
322
|
+
{
|
|
323
|
+
token: {type: ["string", "null"]},
|
|
324
|
+
user: {type: ["object", "null"], "$ref": "#/components/schemas/User"},
|
|
325
|
+
status: {type: ["boolean", "null"]}
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def two_factor_backup_codes_response_schema
|
|
331
|
+
OpenAPI.object_schema(
|
|
332
|
+
{
|
|
333
|
+
status: {type: "boolean"},
|
|
334
|
+
backupCodes: {type: "array", items: {type: "string"}}
|
|
335
|
+
},
|
|
336
|
+
required: ["status", "backupCodes"]
|
|
337
|
+
)
|
|
338
|
+
end
|
|
339
|
+
|
|
298
340
|
def two_factor_schema(config = {})
|
|
299
341
|
custom_schema = config[:schema]
|
|
300
342
|
base = {
|
|
@@ -507,7 +549,7 @@ module BetterAuth
|
|
|
507
549
|
if storage == "hashed"
|
|
508
550
|
Crypto.sha256(code, encoding: :base64url)
|
|
509
551
|
elsif storage == "encrypted"
|
|
510
|
-
Crypto.symmetric_encrypt(key: ctx.context.
|
|
552
|
+
Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: code)
|
|
511
553
|
elsif storage.is_a?(Hash) && storage[:hash].respond_to?(:call)
|
|
512
554
|
storage[:hash].call(code)
|
|
513
555
|
elsif storage.is_a?(Hash) && storage[:encrypt].respond_to?(:call)
|
|
@@ -522,7 +564,7 @@ module BetterAuth
|
|
|
522
564
|
expected, actual = if storage == "hashed"
|
|
523
565
|
[stored, Crypto.sha256(input, encoding: :base64url)]
|
|
524
566
|
elsif storage == "encrypted"
|
|
525
|
-
[Crypto.symmetric_decrypt(key: ctx.context.
|
|
567
|
+
[Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored), input]
|
|
526
568
|
elsif storage.is_a?(Hash) && storage[:hash].respond_to?(:call)
|
|
527
569
|
[stored, storage[:hash].call(input)]
|
|
528
570
|
elsif storage.is_a?(Hash) && storage[:decrypt].respond_to?(:call)
|
|
@@ -53,7 +53,34 @@ module BetterAuth
|
|
|
53
53
|
allowed_media_types: [
|
|
54
54
|
"application/x-www-form-urlencoded",
|
|
55
55
|
"application/json"
|
|
56
|
-
]
|
|
56
|
+
],
|
|
57
|
+
openapi: {
|
|
58
|
+
operationId: "signInUsername",
|
|
59
|
+
description: "Sign in with username and password",
|
|
60
|
+
requestBody: OpenAPI.json_request_body(
|
|
61
|
+
OpenAPI.object_schema(
|
|
62
|
+
{
|
|
63
|
+
username: {type: "string"},
|
|
64
|
+
password: {type: "string"},
|
|
65
|
+
callbackURL: {type: ["string", "null"]},
|
|
66
|
+
rememberMe: {type: ["boolean", "null"]}
|
|
67
|
+
},
|
|
68
|
+
required: ["username", "password"]
|
|
69
|
+
)
|
|
70
|
+
),
|
|
71
|
+
responses: {
|
|
72
|
+
"200" => OpenAPI.json_response(
|
|
73
|
+
"Signed in",
|
|
74
|
+
OpenAPI.object_schema(
|
|
75
|
+
{
|
|
76
|
+
token: {type: "string"},
|
|
77
|
+
user: {type: "object", "$ref": "#/components/schemas/User"}
|
|
78
|
+
},
|
|
79
|
+
required: ["token", "user"]
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
57
84
|
}
|
|
58
85
|
) do |ctx|
|
|
59
86
|
body = normalize_hash(ctx.body)
|
|
@@ -115,7 +142,35 @@ module BetterAuth
|
|
|
115
142
|
end
|
|
116
143
|
|
|
117
144
|
def is_username_available_endpoint(config)
|
|
118
|
-
Endpoint.new(
|
|
145
|
+
Endpoint.new(
|
|
146
|
+
path: "/is-username-available",
|
|
147
|
+
method: "POST",
|
|
148
|
+
metadata: {
|
|
149
|
+
openapi: {
|
|
150
|
+
operationId: "isUsernameAvailable",
|
|
151
|
+
description: "Check whether a username is available",
|
|
152
|
+
requestBody: OpenAPI.json_request_body(
|
|
153
|
+
OpenAPI.object_schema(
|
|
154
|
+
{
|
|
155
|
+
username: {type: "string"}
|
|
156
|
+
},
|
|
157
|
+
required: ["username"]
|
|
158
|
+
)
|
|
159
|
+
),
|
|
160
|
+
responses: {
|
|
161
|
+
"200" => OpenAPI.json_response(
|
|
162
|
+
"Username availability",
|
|
163
|
+
OpenAPI.object_schema(
|
|
164
|
+
{
|
|
165
|
+
available: {type: "boolean"}
|
|
166
|
+
},
|
|
167
|
+
required: ["available"]
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
) do |ctx|
|
|
119
174
|
body = normalize_hash(ctx.body)
|
|
120
175
|
username = body[:username].to_s
|
|
121
176
|
raise APIError.new("UNPROCESSABLE_ENTITY", message: USERNAME_ERROR_CODES["INVALID_USERNAME"]) if username.empty?
|
|
@@ -142,6 +142,7 @@ module BetterAuth
|
|
|
142
142
|
|
|
143
143
|
def storage_for(context, config)
|
|
144
144
|
return [:custom, config[:custom_storage]] if config[:custom_storage]
|
|
145
|
+
return [:database, context.internal_adapter.adapter] if config[:storage] == "database"
|
|
145
146
|
|
|
146
147
|
if config[:storage] == "secondary-storage" && context.options.secondary_storage
|
|
147
148
|
return [:secondary, context.options.secondary_storage]
|
|
@@ -151,6 +152,8 @@ module BetterAuth
|
|
|
151
152
|
end
|
|
152
153
|
|
|
153
154
|
def read_storage((type, storage), key)
|
|
155
|
+
return read_database_storage(storage, key) if type == :database
|
|
156
|
+
|
|
154
157
|
data = storage.get(key)
|
|
155
158
|
data = JSON.parse(data) if type == :secondary && data.is_a?(String)
|
|
156
159
|
normalize_rate_limit_data(symbolize_keys(data))
|
|
@@ -159,12 +162,29 @@ module BetterAuth
|
|
|
159
162
|
end
|
|
160
163
|
|
|
161
164
|
def write_storage((type, storage), key, data, ttl:, update:)
|
|
165
|
+
return write_database_storage(storage, key, data) if type == :database
|
|
166
|
+
|
|
162
167
|
value = (type == :secondary) ? JSON.generate(secondary_storage_data(data)) : data
|
|
163
168
|
return call_secondary_storage_set(storage, key, value, ttl: ttl, update: update) if type == :secondary
|
|
164
169
|
|
|
165
170
|
call_storage_set(storage, key, value, ttl: ttl, update: update)
|
|
166
171
|
end
|
|
167
172
|
|
|
173
|
+
def read_database_storage(adapter, key)
|
|
174
|
+
data = adapter.find_one(model: "rateLimit", where: [{field: "key", value: key}])
|
|
175
|
+
normalize_rate_limit_data(symbolize_keys(data))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def write_database_storage(adapter, key, data)
|
|
179
|
+
value = secondary_storage_data(data)
|
|
180
|
+
existing = adapter.find_one(model: "rateLimit", where: [{field: "key", value: key}])
|
|
181
|
+
if existing
|
|
182
|
+
adapter.update(model: "rateLimit", where: [{field: "key", value: key}], update: value)
|
|
183
|
+
else
|
|
184
|
+
adapter.create(model: "rateLimit", data: value)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
168
188
|
def secondary_storage_data(data)
|
|
169
189
|
{
|
|
170
190
|
key: data[:key],
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
class Response
|
|
7
|
+
attr_reader :status, :headers, :body
|
|
8
|
+
|
|
9
|
+
def initialize(status:, headers:, body:)
|
|
10
|
+
@status = status
|
|
11
|
+
@headers = headers
|
|
12
|
+
@body = body
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.from_rack(tuple)
|
|
16
|
+
status, headers, body = tuple
|
|
17
|
+
new(status: status, headers: headers, body: body)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_a
|
|
21
|
+
[status, headers, body]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
alias_method :to_ary, :to_a
|
|
25
|
+
|
|
26
|
+
def each(&block)
|
|
27
|
+
to_a.each(&block)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def [](index)
|
|
31
|
+
to_a[index]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def first
|
|
35
|
+
status
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def json(**options)
|
|
39
|
+
JSON.parse(body.join, **options)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/better_auth/router.rb
CHANGED
|
@@ -88,6 +88,8 @@ module BetterAuth
|
|
|
88
88
|
error_response(error)
|
|
89
89
|
rescue JSON::ParserError
|
|
90
90
|
error_response(APIError.new("BAD_REQUEST", message: "Invalid JSON body"))
|
|
91
|
+
ensure
|
|
92
|
+
context.clear_runtime! if context.respond_to?(:clear_runtime!)
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
def self.conflicting_methods(entries)
|
|
@@ -360,7 +362,11 @@ module BetterAuth
|
|
|
360
362
|
end
|
|
361
363
|
|
|
362
364
|
def server_only?(endpoint)
|
|
363
|
-
endpoint.metadata[:server_only] ||
|
|
365
|
+
endpoint.metadata[:server_only] ||
|
|
366
|
+
endpoint.metadata[:SERVER_ONLY] ||
|
|
367
|
+
endpoint.metadata["SERVER_ONLY"] ||
|
|
368
|
+
endpoint.metadata[:scope].to_s == "server" ||
|
|
369
|
+
endpoint.metadata["scope"].to_s == "server"
|
|
364
370
|
end
|
|
365
371
|
|
|
366
372
|
def error_response(error, headers: {})
|