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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Plugins
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def dub(options = {})
|
|
8
|
+
config = normalize_hash(options)
|
|
9
|
+
oauth_plugin = dub_oauth_plugin(config[:oauth])
|
|
10
|
+
endpoints = {dub_link: dub_link_endpoint(oauth_plugin)}
|
|
11
|
+
endpoints[:dub_o_auth2_callback] = oauth_plugin.endpoints.fetch(:o_auth2_callback) if oauth_plugin
|
|
12
|
+
|
|
13
|
+
Plugin.new(
|
|
14
|
+
id: "dub",
|
|
15
|
+
endpoints: endpoints,
|
|
16
|
+
init: ->(_context) {
|
|
17
|
+
{
|
|
18
|
+
options: {
|
|
19
|
+
database_hooks: {
|
|
20
|
+
user: {
|
|
21
|
+
create: {
|
|
22
|
+
after: ->(user, ctx) { dub_track_lead(config, user, ctx) }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
options: config
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dub_link_endpoint(oauth_plugin)
|
|
34
|
+
Endpoint.new(
|
|
35
|
+
path: "/dub/link",
|
|
36
|
+
method: "POST",
|
|
37
|
+
metadata: {
|
|
38
|
+
openapi: {
|
|
39
|
+
operationId: "dubLink",
|
|
40
|
+
description: "Link a Dub OAuth account",
|
|
41
|
+
responses: {
|
|
42
|
+
"200" => OpenAPI.json_response(
|
|
43
|
+
"Authorization URL generated successfully for linking a Dub account",
|
|
44
|
+
OpenAPI.object_schema(
|
|
45
|
+
{
|
|
46
|
+
url: {type: "string"},
|
|
47
|
+
redirect: {type: "boolean"}
|
|
48
|
+
},
|
|
49
|
+
required: ["url", "redirect"]
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
) do |ctx|
|
|
56
|
+
unless oauth_plugin
|
|
57
|
+
raise APIError.new("NOT_FOUND", message: "Dub OAuth is not configured")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
body = normalize_hash(ctx.body)
|
|
61
|
+
callback_url = body[:callback_url] || body[:callbackURL]
|
|
62
|
+
if callback_url.to_s.empty?
|
|
63
|
+
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VALIDATION_ERROR"])
|
|
64
|
+
end
|
|
65
|
+
Routes.validate_auth_callback_url!(ctx.context, callback_url, "callbackURL")
|
|
66
|
+
|
|
67
|
+
ctx.body = body.merge(provider_id: "dub", callback_url: callback_url)
|
|
68
|
+
oauth_plugin.endpoints.fetch(:o_auth2_link_account).call(ctx)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def dub_oauth_plugin(oauth_options)
|
|
73
|
+
oauth = normalize_hash(oauth_options || {})
|
|
74
|
+
return nil if oauth.empty?
|
|
75
|
+
|
|
76
|
+
generic_oauth(
|
|
77
|
+
config: [
|
|
78
|
+
{
|
|
79
|
+
provider_id: "dub",
|
|
80
|
+
authorization_url: "https://app.dub.co/oauth/authorize",
|
|
81
|
+
token_url: "https://api.dub.co/oauth/token",
|
|
82
|
+
client_id: oauth[:client_id],
|
|
83
|
+
client_secret: oauth[:client_secret],
|
|
84
|
+
pkce: oauth.key?(:pkce) ? oauth[:pkce] : true
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def dub_track_lead(config, user, ctx)
|
|
91
|
+
return unless ctx
|
|
92
|
+
|
|
93
|
+
dub_id = ctx.get_cookie("dub_id")
|
|
94
|
+
return if dub_id.to_s.empty?
|
|
95
|
+
return if config[:disable_lead_tracking]
|
|
96
|
+
|
|
97
|
+
custom = config[:custom_lead_track]
|
|
98
|
+
if custom.respond_to?(:call)
|
|
99
|
+
custom.call(user, ctx)
|
|
100
|
+
else
|
|
101
|
+
dub_default_lead_track(config, user, dub_id, ctx)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
ctx.set_cookie("dub_id", "", expires: Time.at(0), max_age: 0)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def dub_default_lead_track(config, user, dub_id, ctx)
|
|
108
|
+
track = config[:dub_client]&.track
|
|
109
|
+
return unless track&.respond_to?(:lead)
|
|
110
|
+
|
|
111
|
+
dub_invoke_lead(
|
|
112
|
+
track,
|
|
113
|
+
click_id: dub_id,
|
|
114
|
+
event_name: config[:lead_event_name] || "Sign Up",
|
|
115
|
+
customer_external_id: fetch_value(user, "id"),
|
|
116
|
+
customer_name: fetch_value(user, "name"),
|
|
117
|
+
customer_email: fetch_value(user, "email"),
|
|
118
|
+
customer_avatar: fetch_value(user, "image")
|
|
119
|
+
)
|
|
120
|
+
rescue => error
|
|
121
|
+
dub_log_error(ctx, error)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def dub_log_error(ctx, error)
|
|
125
|
+
logger = ctx.context.logger
|
|
126
|
+
if logger.respond_to?(:error)
|
|
127
|
+
logger.error(error)
|
|
128
|
+
elsif logger.respond_to?(:call)
|
|
129
|
+
logger.call(:error, error)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def dub_invoke_lead(track, payload)
|
|
134
|
+
if track.method(:lead).parameters.any? { |type, name| [:key, :keyreq].include?(type) && name == :request }
|
|
135
|
+
track.lead(request: dub_lead_request_body(payload))
|
|
136
|
+
else
|
|
137
|
+
track.lead(payload)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def dub_lead_request_body(payload)
|
|
142
|
+
klass = defined?(::OpenApiSDK::Models::Operations::TrackLeadRequestBody) && ::OpenApiSDK::Models::Operations::TrackLeadRequestBody
|
|
143
|
+
return payload unless klass
|
|
144
|
+
|
|
145
|
+
klass.new(**payload)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -77,7 +77,28 @@ module BetterAuth
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def send_verification_otp_endpoint(config)
|
|
80
|
-
Endpoint.new(
|
|
80
|
+
Endpoint.new(
|
|
81
|
+
path: "/email-otp/send-verification-otp",
|
|
82
|
+
method: "POST",
|
|
83
|
+
metadata: {
|
|
84
|
+
openapi: {
|
|
85
|
+
operationId: "sendVerificationOTP",
|
|
86
|
+
description: "Send an email verification OTP",
|
|
87
|
+
requestBody: OpenAPI.json_request_body(
|
|
88
|
+
OpenAPI.object_schema(
|
|
89
|
+
{
|
|
90
|
+
email: {type: "string"},
|
|
91
|
+
type: {type: "string", enum: ["email-verification", "sign-in", "forget-password"]}
|
|
92
|
+
},
|
|
93
|
+
required: ["email", "type"]
|
|
94
|
+
)
|
|
95
|
+
),
|
|
96
|
+
responses: {
|
|
97
|
+
"200" => OpenAPI.json_response("OTP sent", OpenAPI.success_response_schema)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
) do |ctx|
|
|
81
102
|
body = normalize_hash(ctx.body)
|
|
82
103
|
email = body[:email].to_s.downcase
|
|
83
104
|
type = body[:type].to_s
|
|
@@ -111,7 +132,30 @@ module BetterAuth
|
|
|
111
132
|
end
|
|
112
133
|
|
|
113
134
|
def get_verification_otp_endpoint(config)
|
|
114
|
-
Endpoint.new(
|
|
135
|
+
Endpoint.new(
|
|
136
|
+
path: "/email-otp/get-verification-otp",
|
|
137
|
+
method: "GET",
|
|
138
|
+
metadata: {
|
|
139
|
+
openapi: {
|
|
140
|
+
operationId: "getVerificationOTP",
|
|
141
|
+
description: "Get a stored verification OTP when storage allows plaintext access",
|
|
142
|
+
parameters: [
|
|
143
|
+
{name: "email", in: "query", required: true, schema: {type: "string"}},
|
|
144
|
+
{name: "type", in: "query", required: true, schema: {type: "string"}}
|
|
145
|
+
],
|
|
146
|
+
responses: {
|
|
147
|
+
"200" => OpenAPI.json_response(
|
|
148
|
+
"Stored OTP",
|
|
149
|
+
OpenAPI.object_schema(
|
|
150
|
+
{
|
|
151
|
+
otp: {type: ["string", "null"]}
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
) do |ctx|
|
|
115
159
|
query = normalize_hash(ctx.query)
|
|
116
160
|
email = query[:email].to_s.downcase
|
|
117
161
|
type = query[:type].to_s
|
|
@@ -124,7 +168,7 @@ module BetterAuth
|
|
|
124
168
|
when "hashed"
|
|
125
169
|
raise APIError.new("BAD_REQUEST", message: "OTP is hashed, cannot return the plain text OTP")
|
|
126
170
|
when "encrypted"
|
|
127
|
-
next ctx.json({otp: Crypto.symmetric_decrypt(key: ctx.context.
|
|
171
|
+
next ctx.json({otp: Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_otp)})
|
|
128
172
|
end
|
|
129
173
|
|
|
130
174
|
storage = config[:store_otp]
|
|
@@ -139,7 +183,29 @@ module BetterAuth
|
|
|
139
183
|
end
|
|
140
184
|
|
|
141
185
|
def check_verification_otp_endpoint(config)
|
|
142
|
-
Endpoint.new(
|
|
186
|
+
Endpoint.new(
|
|
187
|
+
path: "/email-otp/check-verification-otp",
|
|
188
|
+
method: "POST",
|
|
189
|
+
metadata: {
|
|
190
|
+
openapi: {
|
|
191
|
+
operationId: "checkVerificationOTP",
|
|
192
|
+
description: "Check an email verification OTP without consuming it",
|
|
193
|
+
requestBody: OpenAPI.json_request_body(
|
|
194
|
+
OpenAPI.object_schema(
|
|
195
|
+
{
|
|
196
|
+
email: {type: "string"},
|
|
197
|
+
type: {type: "string"},
|
|
198
|
+
otp: {type: "string"}
|
|
199
|
+
},
|
|
200
|
+
required: ["email", "type", "otp"]
|
|
201
|
+
)
|
|
202
|
+
),
|
|
203
|
+
responses: {
|
|
204
|
+
"200" => OpenAPI.json_response("OTP is valid", OpenAPI.success_response_schema)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
) do |ctx|
|
|
143
209
|
body = normalize_hash(ctx.body)
|
|
144
210
|
email = body[:email].to_s.downcase
|
|
145
211
|
type = body[:type].to_s
|
|
@@ -154,7 +220,38 @@ module BetterAuth
|
|
|
154
220
|
end
|
|
155
221
|
|
|
156
222
|
def verify_email_otp_endpoint(config)
|
|
157
|
-
Endpoint.new(
|
|
223
|
+
Endpoint.new(
|
|
224
|
+
path: "/email-otp/verify-email",
|
|
225
|
+
method: "POST",
|
|
226
|
+
metadata: {
|
|
227
|
+
openapi: {
|
|
228
|
+
operationId: "verifyEmailOTP",
|
|
229
|
+
description: "Verify an email address with an OTP",
|
|
230
|
+
requestBody: OpenAPI.json_request_body(
|
|
231
|
+
OpenAPI.object_schema(
|
|
232
|
+
{
|
|
233
|
+
email: {type: "string"},
|
|
234
|
+
otp: {type: "string"}
|
|
235
|
+
},
|
|
236
|
+
required: ["email", "otp"]
|
|
237
|
+
)
|
|
238
|
+
),
|
|
239
|
+
responses: {
|
|
240
|
+
"200" => OpenAPI.json_response(
|
|
241
|
+
"Email verified",
|
|
242
|
+
OpenAPI.object_schema(
|
|
243
|
+
{
|
|
244
|
+
status: {type: "boolean"},
|
|
245
|
+
token: {type: ["string", "null"]},
|
|
246
|
+
user: {type: "object", "$ref": "#/components/schemas/User"}
|
|
247
|
+
},
|
|
248
|
+
required: ["status", "user"]
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
) do |ctx|
|
|
158
255
|
body = normalize_hash(ctx.body)
|
|
159
256
|
email = body[:email].to_s.downcase
|
|
160
257
|
otp = body[:otp].to_s
|
|
@@ -183,7 +280,37 @@ module BetterAuth
|
|
|
183
280
|
end
|
|
184
281
|
|
|
185
282
|
def sign_in_email_otp_endpoint(config)
|
|
186
|
-
Endpoint.new(
|
|
283
|
+
Endpoint.new(
|
|
284
|
+
path: "/sign-in/email-otp",
|
|
285
|
+
method: "POST",
|
|
286
|
+
metadata: {
|
|
287
|
+
openapi: {
|
|
288
|
+
operationId: "signInEmailOTP",
|
|
289
|
+
description: "Sign in with an email OTP",
|
|
290
|
+
requestBody: OpenAPI.json_request_body(
|
|
291
|
+
OpenAPI.object_schema(
|
|
292
|
+
{
|
|
293
|
+
email: {type: "string"},
|
|
294
|
+
otp: {type: "string"}
|
|
295
|
+
},
|
|
296
|
+
required: ["email", "otp"]
|
|
297
|
+
)
|
|
298
|
+
),
|
|
299
|
+
responses: {
|
|
300
|
+
"200" => OpenAPI.json_response(
|
|
301
|
+
"Signed in",
|
|
302
|
+
OpenAPI.object_schema(
|
|
303
|
+
{
|
|
304
|
+
token: {type: "string"},
|
|
305
|
+
user: {type: "object", "$ref": "#/components/schemas/User"}
|
|
306
|
+
},
|
|
307
|
+
required: ["token", "user"]
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
) do |ctx|
|
|
187
314
|
body = normalize_hash(ctx.body)
|
|
188
315
|
email = body[:email].to_s.downcase
|
|
189
316
|
otp = body[:otp].to_s
|
|
@@ -193,9 +320,9 @@ module BetterAuth
|
|
|
193
320
|
user = if found
|
|
194
321
|
found[:user]
|
|
195
322
|
else
|
|
196
|
-
raise APIError.new("BAD_REQUEST", message:
|
|
323
|
+
raise APIError.new("BAD_REQUEST", message: EMAIL_OTP_ERROR_CODES["INVALID_OTP"]) if config[:disable_sign_up]
|
|
197
324
|
|
|
198
|
-
ctx.context.internal_adapter.create_user(email_otp_sign_up_user_data(body, email))
|
|
325
|
+
ctx.context.internal_adapter.create_user(email_otp_sign_up_user_data(ctx, body, email), context: ctx)
|
|
199
326
|
end
|
|
200
327
|
|
|
201
328
|
unless user["emailVerified"]
|
|
@@ -209,7 +336,28 @@ module BetterAuth
|
|
|
209
336
|
end
|
|
210
337
|
|
|
211
338
|
def request_email_change_email_otp_endpoint(config)
|
|
212
|
-
Endpoint.new(
|
|
339
|
+
Endpoint.new(
|
|
340
|
+
path: "/email-otp/request-email-change",
|
|
341
|
+
method: "POST",
|
|
342
|
+
metadata: {
|
|
343
|
+
openapi: {
|
|
344
|
+
operationId: "requestEmailChangeOTP",
|
|
345
|
+
description: "Request an OTP to change the current user's email",
|
|
346
|
+
requestBody: OpenAPI.json_request_body(
|
|
347
|
+
OpenAPI.object_schema(
|
|
348
|
+
{
|
|
349
|
+
newEmail: {type: "string"},
|
|
350
|
+
otp: {type: ["string", "null"]}
|
|
351
|
+
},
|
|
352
|
+
required: ["newEmail"]
|
|
353
|
+
)
|
|
354
|
+
),
|
|
355
|
+
responses: {
|
|
356
|
+
"200" => OpenAPI.json_response("Change email OTP sent", OpenAPI.success_response_schema)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
) do |ctx|
|
|
213
361
|
email_otp_change_email_enabled!(config)
|
|
214
362
|
session = Routes.current_session(ctx)
|
|
215
363
|
body = normalize_hash(ctx.body)
|
|
@@ -235,7 +383,28 @@ module BetterAuth
|
|
|
235
383
|
end
|
|
236
384
|
|
|
237
385
|
def change_email_email_otp_endpoint(config)
|
|
238
|
-
Endpoint.new(
|
|
386
|
+
Endpoint.new(
|
|
387
|
+
path: "/email-otp/change-email",
|
|
388
|
+
method: "POST",
|
|
389
|
+
metadata: {
|
|
390
|
+
openapi: {
|
|
391
|
+
operationId: "changeEmailWithEmailOTP",
|
|
392
|
+
description: "Change the current user's email with an OTP",
|
|
393
|
+
requestBody: OpenAPI.json_request_body(
|
|
394
|
+
OpenAPI.object_schema(
|
|
395
|
+
{
|
|
396
|
+
newEmail: {type: "string"},
|
|
397
|
+
otp: {type: "string"}
|
|
398
|
+
},
|
|
399
|
+
required: ["newEmail", "otp"]
|
|
400
|
+
)
|
|
401
|
+
),
|
|
402
|
+
responses: {
|
|
403
|
+
"200" => OpenAPI.json_response("Email changed", OpenAPI.success_response_schema)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
) do |ctx|
|
|
239
408
|
email_otp_change_email_enabled!(config)
|
|
240
409
|
session = Routes.current_session(ctx)
|
|
241
410
|
body = normalize_hash(ctx.body)
|
|
@@ -259,19 +428,81 @@ module BetterAuth
|
|
|
259
428
|
end
|
|
260
429
|
|
|
261
430
|
def request_password_reset_email_otp_endpoint(config)
|
|
262
|
-
Endpoint.new(
|
|
431
|
+
Endpoint.new(
|
|
432
|
+
path: "/email-otp/request-password-reset",
|
|
433
|
+
method: "POST",
|
|
434
|
+
metadata: {
|
|
435
|
+
openapi: {
|
|
436
|
+
operationId: "requestPasswordResetEmailOTP",
|
|
437
|
+
description: "Request a password reset OTP by email",
|
|
438
|
+
requestBody: OpenAPI.json_request_body(
|
|
439
|
+
OpenAPI.object_schema(
|
|
440
|
+
{
|
|
441
|
+
email: {type: "string"}
|
|
442
|
+
},
|
|
443
|
+
required: ["email"]
|
|
444
|
+
)
|
|
445
|
+
),
|
|
446
|
+
responses: {
|
|
447
|
+
"200" => OpenAPI.json_response("Password reset OTP requested", OpenAPI.status_response_schema)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
) do |ctx|
|
|
263
452
|
email_otp_password_reset_request(ctx, config)
|
|
264
453
|
end
|
|
265
454
|
end
|
|
266
455
|
|
|
267
456
|
def forget_password_email_otp_endpoint(config)
|
|
268
|
-
Endpoint.new(
|
|
457
|
+
Endpoint.new(
|
|
458
|
+
path: "/forget-password/email-otp",
|
|
459
|
+
method: "POST",
|
|
460
|
+
metadata: {
|
|
461
|
+
openapi: {
|
|
462
|
+
operationId: "forgetPasswordEmailOTP",
|
|
463
|
+
description: "Request a password reset OTP by email",
|
|
464
|
+
requestBody: OpenAPI.json_request_body(
|
|
465
|
+
OpenAPI.object_schema(
|
|
466
|
+
{
|
|
467
|
+
email: {type: "string"}
|
|
468
|
+
},
|
|
469
|
+
required: ["email"]
|
|
470
|
+
)
|
|
471
|
+
),
|
|
472
|
+
responses: {
|
|
473
|
+
"200" => OpenAPI.json_response("Password reset OTP requested", OpenAPI.status_response_schema)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
) do |ctx|
|
|
269
478
|
email_otp_password_reset_request(ctx, config)
|
|
270
479
|
end
|
|
271
480
|
end
|
|
272
481
|
|
|
273
482
|
def reset_password_email_otp_endpoint(config)
|
|
274
|
-
Endpoint.new(
|
|
483
|
+
Endpoint.new(
|
|
484
|
+
path: "/email-otp/reset-password",
|
|
485
|
+
method: "POST",
|
|
486
|
+
metadata: {
|
|
487
|
+
openapi: {
|
|
488
|
+
operationId: "resetPasswordEmailOTP",
|
|
489
|
+
description: "Reset a password with an email OTP",
|
|
490
|
+
requestBody: OpenAPI.json_request_body(
|
|
491
|
+
OpenAPI.object_schema(
|
|
492
|
+
{
|
|
493
|
+
email: {type: "string"},
|
|
494
|
+
otp: {type: "string"},
|
|
495
|
+
password: {type: "string"}
|
|
496
|
+
},
|
|
497
|
+
required: ["email", "otp", "password"]
|
|
498
|
+
)
|
|
499
|
+
),
|
|
500
|
+
responses: {
|
|
501
|
+
"200" => OpenAPI.json_response("Password reset", OpenAPI.status_response_schema)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
) do |ctx|
|
|
275
506
|
body = normalize_hash(ctx.body)
|
|
276
507
|
email = body[:email].to_s.downcase
|
|
277
508
|
otp = body[:otp].to_s
|
|
@@ -419,10 +650,21 @@ module BetterAuth
|
|
|
419
650
|
Array.new(config[:otp_length].to_i) { SecureRandom.random_number(10).to_s }.join
|
|
420
651
|
end
|
|
421
652
|
|
|
422
|
-
def email_otp_sign_up_user_data(body, email)
|
|
653
|
+
def email_otp_sign_up_user_data(ctx, body, email)
|
|
423
654
|
reserved = %i[email otp name image callback_url callbackURL callbackUrl]
|
|
424
|
-
|
|
425
|
-
|
|
655
|
+
user_fields = Schema.auth_tables(ctx.context.options).fetch("user").fetch(:fields)
|
|
656
|
+
core_fields = %w[id name email emailVerified image createdAt updatedAt]
|
|
657
|
+
additional = body.each_with_object({}) do |(key, value), result|
|
|
658
|
+
next if reserved.include?(key.to_sym)
|
|
659
|
+
|
|
660
|
+
field = Schema.storage_key(key)
|
|
661
|
+
attributes = user_fields[field]
|
|
662
|
+
next unless attributes
|
|
663
|
+
next if core_fields.include?(field)
|
|
664
|
+
next if attributes[:input] == false
|
|
665
|
+
|
|
666
|
+
result[field] = value
|
|
667
|
+
end
|
|
426
668
|
additional.merge(
|
|
427
669
|
"email" => email,
|
|
428
670
|
"emailVerified" => true,
|
|
@@ -434,7 +676,7 @@ module BetterAuth
|
|
|
434
676
|
def email_otp_stored_value(ctx, config, otp)
|
|
435
677
|
storage = config[:store_otp]
|
|
436
678
|
return Crypto.sha256(otp, encoding: :base64url) if storage.to_s == "hashed"
|
|
437
|
-
return Crypto.symmetric_encrypt(key: ctx.context.
|
|
679
|
+
return Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: otp) if storage.to_s == "encrypted"
|
|
438
680
|
|
|
439
681
|
if storage.is_a?(Hash)
|
|
440
682
|
return storage[:hash].call(otp) if storage[:hash].respond_to?(:call)
|
|
@@ -449,7 +691,7 @@ module BetterAuth
|
|
|
449
691
|
actual, expected = if storage.to_s == "hashed"
|
|
450
692
|
[Crypto.sha256(otp, encoding: :base64url), stored_otp]
|
|
451
693
|
elsif storage.to_s == "encrypted"
|
|
452
|
-
[Crypto.symmetric_decrypt(key: ctx.context.
|
|
694
|
+
[Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_otp), otp]
|
|
453
695
|
elsif storage.is_a?(Hash) && storage[:hash].respond_to?(:call)
|
|
454
696
|
[storage[:hash].call(otp), stored_otp]
|
|
455
697
|
elsif storage.is_a?(Hash) && storage[:decrypt].respond_to?(:call)
|
|
@@ -466,7 +708,7 @@ module BetterAuth
|
|
|
466
708
|
def email_otp_plain_value(ctx, config, stored_otp)
|
|
467
709
|
storage = config[:store_otp]
|
|
468
710
|
return stored_otp if storage.to_s == "plain" || storage.nil?
|
|
469
|
-
return Crypto.symmetric_decrypt(key: ctx.context.
|
|
711
|
+
return Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_otp) if storage.to_s == "encrypted"
|
|
470
712
|
return storage[:decrypt].call(stored_otp) if storage.is_a?(Hash) && storage[:decrypt].respond_to?(:call)
|
|
471
713
|
|
|
472
714
|
nil
|
|
@@ -29,7 +29,23 @@ module BetterAuth
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def expo_authorization_proxy_endpoint
|
|
32
|
-
Endpoint.new(
|
|
32
|
+
Endpoint.new(
|
|
33
|
+
path: "/expo-authorization-proxy",
|
|
34
|
+
method: "GET",
|
|
35
|
+
metadata: {
|
|
36
|
+
openapi: {
|
|
37
|
+
operationId: "expoAuthorizationProxy",
|
|
38
|
+
description: "Proxy an Expo authorization redirect",
|
|
39
|
+
parameters: [
|
|
40
|
+
{in: "query", name: "authorizationURL", required: true, schema: {type: "string", format: "uri"}},
|
|
41
|
+
{in: "query", name: "oauthState", required: false, schema: {type: "string"}}
|
|
42
|
+
],
|
|
43
|
+
responses: {
|
|
44
|
+
"302" => {description: "Redirects to the authorization URL"}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
) do |ctx|
|
|
33
49
|
authorization_url = ctx.query[:authorizationURL] || ctx.query["authorizationURL"] || ctx.query[:authorization_url] || ctx.query["authorization_url"]
|
|
34
50
|
oauth_state = ctx.query[:oauthState] || ctx.query["oauthState"] || ctx.query[:oauth_state] || ctx.query["oauth_state"]
|
|
35
51
|
raise APIError.new("BAD_REQUEST", message: "Unexpected error") if authorization_url.to_s.empty?
|