better_auth 0.4.0 → 0.6.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
|
@@ -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
|
|
@@ -195,7 +322,7 @@ module BetterAuth
|
|
|
195
322
|
else
|
|
196
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(ctx, 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
|
|
@@ -445,7 +676,7 @@ module BetterAuth
|
|
|
445
676
|
def email_otp_stored_value(ctx, config, otp)
|
|
446
677
|
storage = config[:store_otp]
|
|
447
678
|
return Crypto.sha256(otp, encoding: :base64url) if storage.to_s == "hashed"
|
|
448
|
-
return Crypto.symmetric_encrypt(key: ctx.context.
|
|
679
|
+
return Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: otp) if storage.to_s == "encrypted"
|
|
449
680
|
|
|
450
681
|
if storage.is_a?(Hash)
|
|
451
682
|
return storage[:hash].call(otp) if storage[:hash].respond_to?(:call)
|
|
@@ -460,7 +691,7 @@ module BetterAuth
|
|
|
460
691
|
actual, expected = if storage.to_s == "hashed"
|
|
461
692
|
[Crypto.sha256(otp, encoding: :base64url), stored_otp]
|
|
462
693
|
elsif storage.to_s == "encrypted"
|
|
463
|
-
[Crypto.symmetric_decrypt(key: ctx.context.
|
|
694
|
+
[Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_otp), otp]
|
|
464
695
|
elsif storage.is_a?(Hash) && storage[:hash].respond_to?(:call)
|
|
465
696
|
[storage[:hash].call(otp), stored_otp]
|
|
466
697
|
elsif storage.is_a?(Hash) && storage[:decrypt].respond_to?(:call)
|
|
@@ -477,7 +708,7 @@ module BetterAuth
|
|
|
477
708
|
def email_otp_plain_value(ctx, config, stored_otp)
|
|
478
709
|
storage = config[:store_otp]
|
|
479
710
|
return stored_otp if storage.to_s == "plain" || storage.nil?
|
|
480
|
-
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"
|
|
481
712
|
return storage[:decrypt].call(stored_otp) if storage.is_a?(Hash) && storage[:decrypt].respond_to?(:call)
|
|
482
713
|
|
|
483
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?
|
|
@@ -213,7 +213,19 @@ module BetterAuth
|
|
|
213
213
|
end
|
|
214
214
|
|
|
215
215
|
def sign_in_with_oauth2_endpoint(config)
|
|
216
|
-
Endpoint.new(
|
|
216
|
+
Endpoint.new(
|
|
217
|
+
path: "/sign-in/oauth2",
|
|
218
|
+
method: "POST",
|
|
219
|
+
metadata: {
|
|
220
|
+
openapi: {
|
|
221
|
+
operationId: "signInOAuth2",
|
|
222
|
+
description: "Sign in with OAuth2",
|
|
223
|
+
responses: {
|
|
224
|
+
"200" => OpenAPI.json_response("Sign in with OAuth2", generic_oauth_url_response_schema)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
) do |ctx|
|
|
217
229
|
body = normalize_hash(ctx.body)
|
|
218
230
|
provider_id = body[:provider_id].to_s
|
|
219
231
|
provider = generic_oauth_provider!(config, provider_id)
|
|
@@ -223,7 +235,19 @@ module BetterAuth
|
|
|
223
235
|
end
|
|
224
236
|
|
|
225
237
|
def o_auth2_link_account_endpoint(config)
|
|
226
|
-
Endpoint.new(
|
|
238
|
+
Endpoint.new(
|
|
239
|
+
path: "/oauth2/link",
|
|
240
|
+
method: "POST",
|
|
241
|
+
metadata: {
|
|
242
|
+
openapi: {
|
|
243
|
+
operationId: "linkOAuth2",
|
|
244
|
+
description: "Link an OAuth2 account to the current user session",
|
|
245
|
+
responses: {
|
|
246
|
+
"200" => OpenAPI.json_response("Authorization URL generated successfully for linking an OAuth2 account", generic_oauth_url_response_schema)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
) do |ctx|
|
|
227
251
|
session = Routes.current_session(ctx)
|
|
228
252
|
body = normalize_hash(ctx.body)
|
|
229
253
|
provider_id = body[:provider_id].to_s
|
|
@@ -243,8 +267,20 @@ module BetterAuth
|
|
|
243
267
|
def o_auth2_callback_endpoint(config)
|
|
244
268
|
Endpoint.new(
|
|
245
269
|
path: "/oauth2/callback/:providerId",
|
|
246
|
-
method:
|
|
247
|
-
metadata: {
|
|
270
|
+
method: "GET",
|
|
271
|
+
metadata: {
|
|
272
|
+
allowed_media_types: ["application/x-www-form-urlencoded", "application/json"],
|
|
273
|
+
openapi: {
|
|
274
|
+
operationId: "oauth2Callback",
|
|
275
|
+
description: "OAuth2 callback",
|
|
276
|
+
responses: {
|
|
277
|
+
"200" => OpenAPI.json_response(
|
|
278
|
+
"OAuth2 callback",
|
|
279
|
+
OpenAPI.object_schema({url: {type: "string"}}, required: ["url"])
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
248
284
|
) do |ctx|
|
|
249
285
|
query = normalize_hash(ctx.query)
|
|
250
286
|
provider_id = (fetch_value(ctx.params, "providerId") || query[:provider_id]).to_s
|
|
@@ -306,6 +342,16 @@ module BetterAuth
|
|
|
306
342
|
end
|
|
307
343
|
end
|
|
308
344
|
|
|
345
|
+
def generic_oauth_url_response_schema
|
|
346
|
+
OpenAPI.object_schema(
|
|
347
|
+
{
|
|
348
|
+
url: {type: "string"},
|
|
349
|
+
redirect: {type: "boolean"}
|
|
350
|
+
},
|
|
351
|
+
required: ["url", "redirect"]
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
309
355
|
def generic_oauth_authorization_url(ctx, provider, body, link:)
|
|
310
356
|
authorization_url = provider[:authorization_url] || generic_oauth_discovery(provider)["authorization_endpoint"]
|
|
311
357
|
token_url = provider[:token_url] || generic_oauth_discovery(provider)["token_endpoint"]
|
|
@@ -368,7 +414,7 @@ module BetterAuth
|
|
|
368
414
|
state = Crypto.random_string(32)
|
|
369
415
|
if strategy.to_s == "cookie"
|
|
370
416
|
cookie = ctx.context.create_auth_cookie("oauth_state", max_age: 600)
|
|
371
|
-
encrypted = Crypto.symmetric_encrypt(key: ctx.context.
|
|
417
|
+
encrypted = Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: JSON.generate(state_data.merge("state" => state)))
|
|
372
418
|
ctx.set_cookie(cookie.name, encrypted, cookie.attributes)
|
|
373
419
|
return state
|
|
374
420
|
end
|
|
@@ -416,7 +462,7 @@ module BetterAuth
|
|
|
416
462
|
end
|
|
417
463
|
|
|
418
464
|
begin
|
|
419
|
-
decrypted = Crypto.symmetric_decrypt(key: ctx.context.
|
|
465
|
+
decrypted = Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: encrypted)
|
|
420
466
|
unless decrypted
|
|
421
467
|
Cookies.expire_cookie(ctx, cookie)
|
|
422
468
|
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "please_restart_the_process"))
|
|
@@ -525,7 +571,7 @@ module BetterAuth
|
|
|
525
571
|
return token if token.to_s.empty?
|
|
526
572
|
return token unless ctx.context.options.account[:encrypt_oauth_tokens]
|
|
527
573
|
|
|
528
|
-
Crypto.symmetric_encrypt(key: ctx.context.
|
|
574
|
+
Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: token)
|
|
529
575
|
end
|
|
530
576
|
|
|
531
577
|
def generic_oauth_set_account_cookie(ctx, provider_id, account_id, user_id)
|