better_auth 0.8.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +92 -0
- data/LICENSE.md +1 -1
- data/README.md +142 -95
- data/lib/better_auth/adapters/base.rb +64 -1
- data/lib/better_auth/adapters/internal_adapter.rb +197 -11
- data/lib/better_auth/adapters/join_support.rb +10 -0
- data/lib/better_auth/adapters/memory.rb +303 -54
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +390 -63
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +48 -47
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +119 -15
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +108 -0
- data/lib/better_auth/endpoint.rb +88 -5
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/http_client.rb +46 -0
- data/lib/better_auth/logger.rb +1 -1
- data/lib/better_auth/middleware/origin_check.rb +9 -1
- data/lib/better_auth/migration_plan.rb +15 -0
- data/lib/better_auth/oauth2.rb +1 -1
- data/lib/better_auth/oauth_state.rb +147 -0
- data/lib/better_auth/plugin_loader.rb +220 -0
- data/lib/better_auth/plugins/access.rb +10 -13
- data/lib/better_auth/plugins/admin.rb +85 -29
- data/lib/better_auth/plugins/anonymous.rb +2 -0
- data/lib/better_auth/plugins/api_key.rb +4 -7
- data/lib/better_auth/plugins/bearer.rb +8 -22
- data/lib/better_auth/plugins/captcha.rb +24 -6
- data/lib/better_auth/plugins/device_authorization.rb +77 -5
- data/lib/better_auth/plugins/dub.rb +10 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +162 -140
- data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +44 -19
- data/lib/better_auth/plugins/magic_link.rb +18 -21
- data/lib/better_auth/plugins/oauth_popup.rb +330 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +233 -46
- data/lib/better_auth/plugins/oauth_provider.rb +4 -7
- data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
- data/lib/better_auth/plugins/one_tap.rb +78 -65
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +77 -9
- data/lib/better_auth/plugins/organization.rb +546 -102
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +25 -27
- data/lib/better_auth/plugins/scim.rb +4 -7
- data/lib/better_auth/plugins/siwe.rb +126 -16
- data/lib/better_auth/plugins/sso.rb +4 -7
- data/lib/better_auth/plugins/stripe.rb +4 -7
- data/lib/better_auth/plugins/two_factor.rb +205 -19
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +300 -133
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +44 -8
- data/lib/better_auth/routes/email_verification.rb +93 -6
- data/lib/better_auth/routes/password.rb +6 -6
- data/lib/better_auth/routes/session.rb +7 -2
- data/lib/better_auth/routes/sign_in.rb +10 -1
- data/lib/better_auth/routes/sign_up.rb +45 -22
- data/lib/better_auth/routes/social.rb +197 -54
- data/lib/better_auth/routes/user.rb +17 -10
- data/lib/better_auth/schema/sql.rb +189 -34
- data/lib/better_auth/schema.rb +159 -5
- data/lib/better_auth/session.rb +29 -10
- data/lib/better_auth/session_store.rb +1 -1
- data/lib/better_auth/social_providers/apple.rb +6 -1
- data/lib/better_auth/social_providers/atlassian.rb +1 -0
- data/lib/better_auth/social_providers/base.rb +45 -23
- data/lib/better_auth/social_providers/cognito.rb +75 -5
- data/lib/better_auth/social_providers/facebook.rb +95 -1
- data/lib/better_auth/social_providers/figma.rb +2 -0
- data/lib/better_auth/social_providers/github.rb +2 -0
- data/lib/better_auth/social_providers/google.rb +12 -1
- data/lib/better_auth/social_providers/line.rb +35 -1
- data/lib/better_auth/social_providers/linear.rb +1 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
- data/lib/better_auth/social_providers/naver.rb +24 -1
- data/lib/better_auth/social_providers/notion.rb +1 -0
- data/lib/better_auth/social_providers/paybin.rb +25 -1
- data/lib/better_auth/social_providers/paypal.rb +118 -2
- data/lib/better_auth/social_providers/reddit.rb +25 -4
- data/lib/better_auth/social_providers/salesforce.rb +1 -0
- data/lib/better_auth/social_providers/twitch.rb +24 -1
- data/lib/better_auth/social_providers/twitter.rb +36 -1
- data/lib/better_auth/social_providers/vk.rb +34 -1
- data/lib/better_auth/social_providers/wechat.rb +6 -4
- data/lib/better_auth/sql_migration.rb +566 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +14 -41
- metadata +14 -18
- data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
- data/lib/better_auth/plugins/mcp/config.rb +0 -51
- data/lib/better_auth/plugins/mcp/consent.rb +0 -31
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
- data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
- data/lib/better_auth/plugins/mcp/registration.rb +0 -31
- data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
- data/lib/better_auth/plugins/mcp/schema.rb +0 -91
- data/lib/better_auth/plugins/mcp/token.rb +0 -108
- data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
- data/lib/better_auth/plugins/mcp.rb +0 -215
- data/lib/better_auth/plugins/oidc_provider.rb +0 -710
|
@@ -58,6 +58,7 @@ module BetterAuth
|
|
|
58
58
|
if email_config[:enabled] != true || email_config[:disable_sign_up]
|
|
59
59
|
raise APIError.new("BAD_REQUEST", code: "EMAIL_PASSWORD_SIGN_UP_DISABLED", message: BASE_ERROR_CODES["EMAIL_PASSWORD_SIGN_UP_DISABLED"])
|
|
60
60
|
end
|
|
61
|
+
preflight_sign_up_token_link!(ctx)
|
|
61
62
|
|
|
62
63
|
body = normalize_hash(ctx.body)
|
|
63
64
|
name = body["name"].to_s
|
|
@@ -69,14 +70,16 @@ module BetterAuth
|
|
|
69
70
|
|
|
70
71
|
validate_auth_callback_url!(ctx.context, callback_url, "callbackURL")
|
|
71
72
|
validate_sign_up_input!(email, password, email_config)
|
|
73
|
+
reserved = %w[email password name image callbackURL callbackUrl callback_url rememberMe remember_me]
|
|
74
|
+
additional_user_fields = parse_sign_up_additional_user_fields(ctx, body.except(*reserved))
|
|
72
75
|
|
|
73
76
|
ctx.context.adapter.transaction do
|
|
74
77
|
existing = ctx.context.internal_adapter.find_user_by_email(email)
|
|
75
78
|
if existing
|
|
76
|
-
if email_config[:require_email_verification]
|
|
79
|
+
if email_config[:require_email_verification] || email_config[:auto_sign_in] == false
|
|
77
80
|
hash_password(ctx, password)
|
|
78
81
|
call_existing_sign_up_callback(ctx, email_config, existing)
|
|
79
|
-
synthetic_user = synthetic_sign_up_user(ctx,
|
|
82
|
+
synthetic_user = synthetic_sign_up_user(ctx, additional_user_fields, email, name, image)
|
|
80
83
|
next ctx.json({token: nil, user: Schema.parse_output(options, "user", synthetic_user)})
|
|
81
84
|
end
|
|
82
85
|
|
|
@@ -87,7 +90,7 @@ module BetterAuth
|
|
|
87
90
|
end
|
|
88
91
|
|
|
89
92
|
hashed_password = hash_password(ctx, password)
|
|
90
|
-
created_user = create_sign_up_user(ctx,
|
|
93
|
+
created_user = create_sign_up_user(ctx, additional_user_fields, email, name, image)
|
|
91
94
|
raise APIError.new("UNPROCESSABLE_ENTITY", message: BASE_ERROR_CODES["FAILED_TO_CREATE_USER"]) unless created_user
|
|
92
95
|
|
|
93
96
|
ctx.context.internal_adapter.link_account(
|
|
@@ -144,9 +147,7 @@ module BetterAuth
|
|
|
144
147
|
raise APIError.new("FORBIDDEN", message: "Invalid #{label}")
|
|
145
148
|
end
|
|
146
149
|
|
|
147
|
-
def self.create_sign_up_user(ctx,
|
|
148
|
-
reserved = %w[email password name image callbackURL callbackUrl callback_url rememberMe remember_me]
|
|
149
|
-
additional = parse_declared_input(ctx, "user", body.except(*reserved), allowed_base: [])
|
|
150
|
+
def self.create_sign_up_user(ctx, additional, email, name, image)
|
|
150
151
|
ctx.context.internal_adapter.create_user(
|
|
151
152
|
additional.merge(
|
|
152
153
|
"email" => email.downcase,
|
|
@@ -175,7 +176,7 @@ module BetterAuth
|
|
|
175
176
|
end
|
|
176
177
|
end
|
|
177
178
|
|
|
178
|
-
def self.synthetic_sign_up_user(ctx,
|
|
179
|
+
def self.synthetic_sign_up_user(ctx, additional, email, name, image)
|
|
179
180
|
now = Time.now
|
|
180
181
|
core_fields = {
|
|
181
182
|
"id" => SecureRandom.hex(16),
|
|
@@ -186,8 +187,6 @@ module BetterAuth
|
|
|
186
187
|
"createdAt" => now,
|
|
187
188
|
"updatedAt" => now
|
|
188
189
|
}
|
|
189
|
-
reserved = %w[email password name image callbackURL callbackUrl callback_url rememberMe remember_me]
|
|
190
|
-
additional = synthetic_additional_user_fields(ctx, body.except(*reserved))
|
|
191
190
|
custom = ctx.context.options.email_and_password[:custom_synthetic_user]
|
|
192
191
|
return core_fields.merge(additional) unless custom.respond_to?(:call)
|
|
193
192
|
|
|
@@ -201,20 +200,37 @@ module BetterAuth
|
|
|
201
200
|
stringify_synthetic_user(custom.call(value))
|
|
202
201
|
end
|
|
203
202
|
|
|
204
|
-
def self.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
203
|
+
def self.parse_sign_up_additional_user_fields(ctx, data)
|
|
204
|
+
input = normalize_hash(data || {})
|
|
205
|
+
fields = Schema.auth_tables(ctx.context.options).fetch("user").fetch(:fields)
|
|
206
|
+
.except(*core_model_fields("user"))
|
|
207
|
+
|
|
208
|
+
fields.each_with_object({}) do |(field, attributes), result|
|
|
209
|
+
if input.key?(field)
|
|
210
|
+
if attributes[:input] == false
|
|
211
|
+
if attributes.key?(:default_value)
|
|
212
|
+
result[field] = resolve_default(attributes[:default_value])
|
|
213
|
+
elsif js_truthy_input?(input[field])
|
|
214
|
+
raise APIError.new("BAD_REQUEST", message: "#{field} is not allowed to be set")
|
|
215
|
+
end
|
|
216
|
+
next
|
|
217
|
+
end
|
|
213
218
|
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
result[field] = coerce_input_value(input[field], attributes)
|
|
220
|
+
elsif attributes.key?(:default_value)
|
|
221
|
+
result[field] = resolve_default(attributes[:default_value])
|
|
222
|
+
elsif attributes[:required]
|
|
223
|
+
raise APIError.new("BAD_REQUEST", message: "#{field} is required")
|
|
224
|
+
end
|
|
216
225
|
end
|
|
217
|
-
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def self.js_truthy_input?(value)
|
|
229
|
+
return false if value.nil? || value == false
|
|
230
|
+
return false if value.is_a?(Numeric) && (value.zero? || (value.respond_to?(:nan?) && value.nan?))
|
|
231
|
+
return false if value.is_a?(String) && value.empty?
|
|
232
|
+
|
|
233
|
+
true
|
|
218
234
|
end
|
|
219
235
|
|
|
220
236
|
def self.resolve_default(value)
|
|
@@ -242,10 +258,17 @@ module BetterAuth
|
|
|
242
258
|
expires_in: verification[:expires_in] || 3600
|
|
243
259
|
)
|
|
244
260
|
callback = URI.encode_www_form_component(callback_url || "/")
|
|
245
|
-
url = "#{ctx.context.
|
|
261
|
+
url = "#{ctx.context.token_link_base_url}/verify-email?token=#{URI.encode_www_form_component(token)}&callbackURL=#{callback}"
|
|
246
262
|
sender.call({user: user, url: url, token: token}, ctx.request)
|
|
247
263
|
end
|
|
248
264
|
|
|
265
|
+
def self.preflight_sign_up_token_link!(ctx)
|
|
266
|
+
verification = ctx.context.options.email_verification
|
|
267
|
+
password_config = ctx.context.options.email_and_password
|
|
268
|
+
send_on_sign_up = verification.key?(:send_on_sign_up) ? verification[:send_on_sign_up] : password_config[:require_email_verification]
|
|
269
|
+
ctx.context.token_link_base_url if send_on_sign_up && verification[:send_verification_email].respond_to?(:call)
|
|
270
|
+
end
|
|
271
|
+
|
|
249
272
|
def self.session_overrides(ctx)
|
|
250
273
|
{
|
|
251
274
|
ipAddress: RequestIP.client_ip(ctx, ctx.context.options).to_s,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "json"
|
|
5
|
+
require "net/http"
|
|
5
6
|
require "securerandom"
|
|
6
7
|
|
|
7
8
|
module BetterAuth
|
|
@@ -92,12 +93,13 @@ module BetterAuth
|
|
|
92
93
|
ctx.context.secret,
|
|
93
94
|
expires_in: 600
|
|
94
95
|
)
|
|
96
|
+
store_oauth_state_cookie(ctx, state)
|
|
95
97
|
url = call_provider(provider, :create_authorization_url, {
|
|
96
98
|
state: state,
|
|
97
99
|
codeVerifier: code_verifier,
|
|
98
100
|
code_verifier: code_verifier,
|
|
99
|
-
redirectURI: "#{ctx.context.
|
|
100
|
-
redirect_uri: "#{ctx.context.
|
|
101
|
+
redirectURI: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
102
|
+
redirect_uri: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
101
103
|
scopes: body["scopes"],
|
|
102
104
|
loginHint: body["loginHint"] || body["login_hint"]
|
|
103
105
|
})
|
|
@@ -133,38 +135,56 @@ module BetterAuth
|
|
|
133
135
|
if ctx.method == "POST"
|
|
134
136
|
merged = normalize_hash(ctx.query).merge(normalize_hash(ctx.body))
|
|
135
137
|
query = URI.encode_www_form(merged.reject { |_key, value| value.nil? || value.to_s.empty? })
|
|
136
|
-
target = "#{ctx.context.
|
|
138
|
+
target = "#{ctx.context.canonical_base_url}/callback/#{provider_id}"
|
|
137
139
|
target = "#{target}?#{query}" unless query.empty?
|
|
138
140
|
raise ctx.redirect(target)
|
|
139
141
|
end
|
|
140
142
|
|
|
141
143
|
source = ctx.query
|
|
142
144
|
data = normalize_hash(source)
|
|
143
|
-
provider = social_provider(ctx.context, provider_id)
|
|
144
145
|
state = data["state"].to_s
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
default_error_url = ctx.context.options.on_api_error[:error_url] || "#{ctx.context.base_url}/error"
|
|
147
|
+
state_data = begin
|
|
148
|
+
OAuthState.parse(ctx, state)
|
|
149
|
+
rescue OAuthState::Error => error
|
|
150
|
+
raise ctx.redirect(oauth_error_url(error.error_url || default_error_url, error.code))
|
|
151
|
+
end
|
|
152
|
+
error_url = state_data["errorURL"] || state_data["errorCallbackURL"] || default_error_url
|
|
147
153
|
|
|
148
154
|
raise ctx.redirect(oauth_error_url(error_url, data["error"], data["errorDescription"] || data["error_description"])) if data["error"]
|
|
149
|
-
raise ctx.redirect(oauth_error_url(error_url, "oauth_provider_not_found")) unless provider
|
|
150
|
-
raise ctx.redirect(oauth_error_url(error_url, "state_not_found")) unless state_data
|
|
151
155
|
raise ctx.redirect(oauth_error_url(error_url, "no_code")) if data["code"].to_s.empty?
|
|
156
|
+
provider = social_provider(ctx.context, provider_id)
|
|
157
|
+
raise ctx.redirect(oauth_error_url(error_url, "oauth_provider_not_found")) unless provider
|
|
152
158
|
|
|
153
|
-
tokens =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
tokens = begin
|
|
160
|
+
call_provider(provider, :validate_authorization_code, {
|
|
161
|
+
code: data["code"],
|
|
162
|
+
codeVerifier: state_data["codeVerifier"],
|
|
163
|
+
code_verifier: state_data["codeVerifier"],
|
|
164
|
+
redirectURI: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
165
|
+
redirect_uri: "#{ctx.context.canonical_base_url}/callback/#{provider_id}"
|
|
166
|
+
})
|
|
167
|
+
rescue APIError => error
|
|
168
|
+
raise if error.status == "FOUND"
|
|
169
|
+
|
|
170
|
+
nil
|
|
171
|
+
rescue => error
|
|
172
|
+
social_log(ctx.context, :error, "OAuth code validation failed: #{error.message}")
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
160
175
|
raise ctx.redirect(oauth_error_url(error_url, "invalid_code")) unless tokens
|
|
161
176
|
|
|
162
177
|
token_data = token_hash(tokens)
|
|
163
178
|
token_data["user"] = parse_json_hash(data["user"]) if data["user"]
|
|
164
|
-
user_info =
|
|
179
|
+
user_info = begin
|
|
180
|
+
call_provider(provider, :get_user_info, token_data)
|
|
181
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, SystemCallError
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
165
184
|
user = user_info[:user] || user_info["user"] if user_info
|
|
166
185
|
raise ctx.redirect(oauth_error_url(error_url, "unable_to_get_user_info")) unless user
|
|
167
186
|
raise ctx.redirect(oauth_error_url(error_url, "email_not_found")) if fetch_value(user, "email").to_s.empty?
|
|
187
|
+
raise ctx.redirect(oauth_error_url(error_url, "unable_to_get_user_info")) if blank_remote_id?(fetch_value(user, "id"))
|
|
168
188
|
|
|
169
189
|
link = state_data["link"] || state_data[:link]
|
|
170
190
|
if link
|
|
@@ -185,7 +205,7 @@ module BetterAuth
|
|
|
185
205
|
)
|
|
186
206
|
raise ctx.redirect(oauth_error_url(error_url, session_data[:error].tr(" ", "_"))) if session_data[:error]
|
|
187
207
|
Cookies.set_session_cookie(ctx, session_data)
|
|
188
|
-
callback_url = session_data[:new_user] ? (state_data["newUserCallbackURL"] || state_data["callbackURL"] || "/") : (state_data["callbackURL"] || "/")
|
|
208
|
+
callback_url = session_data[:new_user] ? (state_data["newUserURL"] || state_data["newUserCallbackURL"] || state_data["callbackURL"] || "/") : (state_data["callbackURL"] || "/")
|
|
189
209
|
raise ctx.redirect(callback_url)
|
|
190
210
|
end
|
|
191
211
|
end
|
|
@@ -251,8 +271,15 @@ module BetterAuth
|
|
|
251
271
|
account["providerId"] == provider_id && account["accountId"] == account_id
|
|
252
272
|
end
|
|
253
273
|
unless existing
|
|
254
|
-
|
|
274
|
+
begin
|
|
275
|
+
account = ctx.context.internal_adapter.create_account(token_hash_for_storage(ctx, data[:account]).merge("userId" => session[:user]["id"]))
|
|
276
|
+
raise BetterAuth::Error, "Unable to link social account" unless account
|
|
277
|
+
rescue
|
|
278
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
279
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: "Unable to link social account")
|
|
280
|
+
end
|
|
255
281
|
end
|
|
282
|
+
update_social_user_info_on_link(ctx, session[:user]["id"], data[:user])
|
|
256
283
|
update_verified_email_on_link(ctx, session[:user]["id"], session[:user]["email"], data[:user])
|
|
257
284
|
next ctx.json({url: "", status: true, redirect: false})
|
|
258
285
|
end
|
|
@@ -269,12 +296,13 @@ module BetterAuth
|
|
|
269
296
|
}
|
|
270
297
|
}.merge(safe_additional_state(body))
|
|
271
298
|
state = Crypto.sign_jwt(state_data, ctx.context.secret, expires_in: 600)
|
|
299
|
+
store_oauth_state_cookie(ctx, state)
|
|
272
300
|
url = call_provider(provider, :create_authorization_url, {
|
|
273
301
|
state: state,
|
|
274
302
|
codeVerifier: code_verifier,
|
|
275
303
|
code_verifier: code_verifier,
|
|
276
|
-
redirectURI: "#{ctx.context.
|
|
277
|
-
redirect_uri: "#{ctx.context.
|
|
304
|
+
redirectURI: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
305
|
+
redirect_uri: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
278
306
|
scopes: body["scopes"],
|
|
279
307
|
loginHint: body["loginHint"] || body["login_hint"]
|
|
280
308
|
})
|
|
@@ -285,6 +313,10 @@ module BetterAuth
|
|
|
285
313
|
|
|
286
314
|
def self.social_user_from_id_token!(ctx, provider, id_token)
|
|
287
315
|
token = fetch_value(id_token, "token").to_s
|
|
316
|
+
unless provider_callable(provider, :verify_id_token)
|
|
317
|
+
raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES["ID_TOKEN_NOT_SUPPORTED"])
|
|
318
|
+
end
|
|
319
|
+
|
|
288
320
|
valid = call_provider(provider, :verify_id_token, token, fetch_value(id_token, "nonce"))
|
|
289
321
|
raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["INVALID_TOKEN"]) unless valid
|
|
290
322
|
|
|
@@ -301,6 +333,7 @@ module BetterAuth
|
|
|
301
333
|
user = user_info[:user] || user_info["user"] if user_info
|
|
302
334
|
raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["FAILED_TO_GET_USER_INFO"]) unless user
|
|
303
335
|
raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["USER_EMAIL_NOT_FOUND"]) if fetch_value(user, "email").to_s.empty?
|
|
336
|
+
raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["FAILED_TO_GET_USER_INFO"]) if blank_remote_id?(fetch_value(user, "id"))
|
|
304
337
|
|
|
305
338
|
{
|
|
306
339
|
user: user,
|
|
@@ -315,51 +348,105 @@ module BetterAuth
|
|
|
315
348
|
}
|
|
316
349
|
end
|
|
317
350
|
|
|
318
|
-
def self.persist_social_user(ctx, provider_id, user_info, account_info, callback_url: nil, disable_sign_up: false)
|
|
351
|
+
def self.persist_social_user(ctx, provider_id, user_info, account_info, callback_url: nil, disable_sign_up: false, override_user_info: false)
|
|
319
352
|
email = fetch_value(user_info, "email").to_s.downcase
|
|
320
353
|
account_id = (account_info["accountId"] || account_info[:accountId] || account_info[:account_id] || fetch_value(user_info, "id")).to_s
|
|
321
|
-
|
|
354
|
+
return {error: "unable to get user info"} if blank_remote_id?(account_id)
|
|
355
|
+
|
|
356
|
+
existing = begin
|
|
357
|
+
ctx.context.internal_adapter.find_oauth_user(email, account_id, provider_id)
|
|
358
|
+
rescue
|
|
359
|
+
social_log(ctx.context, :error, "Unable to query social user")
|
|
360
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: "internal server error")
|
|
361
|
+
end
|
|
322
362
|
|
|
323
363
|
if existing && existing[:linked_account]
|
|
324
364
|
user = existing[:user]
|
|
325
365
|
if ctx.context.options.account[:update_account_on_sign_in] != false
|
|
326
366
|
update_data = account_storage_fields(account_info)
|
|
327
|
-
|
|
367
|
+
unless update_data.empty?
|
|
368
|
+
begin
|
|
369
|
+
ctx.context.internal_adapter.update_account(existing[:linked_account]["id"], update_data)
|
|
370
|
+
rescue
|
|
371
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
372
|
+
raise
|
|
373
|
+
end
|
|
374
|
+
end
|
|
328
375
|
end
|
|
329
376
|
verified_user = update_verified_email_on_link(ctx, user["id"], user["email"], user_info)
|
|
330
377
|
user = verified_user if verified_user
|
|
331
378
|
new_user = false
|
|
332
379
|
elsif existing
|
|
333
|
-
|
|
334
|
-
|
|
380
|
+
linkable = linkable_provider?(ctx, provider_id, user_info, implicit: true, local_user: existing[:user])
|
|
381
|
+
unless linkable
|
|
382
|
+
return {error: existing[:user]["banned"] ? "banned" : "account not linked"}
|
|
335
383
|
end
|
|
336
384
|
user = existing[:user]
|
|
337
|
-
ctx.context.
|
|
338
|
-
|
|
339
|
-
|
|
385
|
+
user = ctx.context.adapter.transaction do
|
|
386
|
+
account = begin
|
|
387
|
+
ctx.context.internal_adapter.create_account(account_info.merge("providerId" => provider_id, "accountId" => account_id, "userId" => user["id"]))
|
|
388
|
+
rescue
|
|
389
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
390
|
+
raise
|
|
391
|
+
end
|
|
392
|
+
unless account
|
|
393
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
394
|
+
raise BetterAuth::Error, "Failed to link social account"
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
promoted = promote_verified_email_on_link!(ctx, user, user_info)
|
|
398
|
+
linked_user = promoted || user
|
|
399
|
+
update_social_user_info_on_link(ctx, linked_user["id"], user_info) || linked_user
|
|
400
|
+
end
|
|
340
401
|
new_user = false
|
|
341
402
|
else
|
|
342
403
|
return {error: "signup disabled"} if disable_sign_up
|
|
343
404
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
405
|
+
begin
|
|
406
|
+
provider_fields = Schema.parse_provider_profile_user_input(ctx.context.options, user_info, action: :create)
|
|
407
|
+
created = ctx.context.internal_adapter.create_oauth_user(
|
|
408
|
+
provider_fields.merge(
|
|
409
|
+
email: email,
|
|
410
|
+
name: fetch_value(user_info, "name").to_s,
|
|
411
|
+
image: fetch_value(user_info, "image"),
|
|
412
|
+
emailVerified: !!fetch_value(user_info, "emailVerified")
|
|
413
|
+
),
|
|
414
|
+
account_info.merge("providerId" => provider_id, "accountId" => account_id),
|
|
415
|
+
context: ctx
|
|
416
|
+
)
|
|
417
|
+
raise BetterAuth::Error, "Failed to create social user" unless created
|
|
418
|
+
rescue
|
|
419
|
+
social_log(ctx.context, :error, "Unable to create social user")
|
|
420
|
+
return {error: "unable to create user"}
|
|
421
|
+
end
|
|
354
422
|
user = created[:user]
|
|
355
423
|
new_user = true
|
|
356
424
|
end
|
|
357
|
-
|
|
425
|
+
if existing && (override_user_info || provider_override_user_info_on_sign_in?(provider_id, ctx.context))
|
|
426
|
+
user = override_social_user_info(ctx, user, user_info)
|
|
427
|
+
end
|
|
428
|
+
return {error: "unable to create user"} unless user
|
|
358
429
|
|
|
359
430
|
session = ctx.context.internal_adapter.create_session(user["id"], false, session_overrides(ctx), true, ctx)
|
|
360
431
|
{session: session, user: user, new_user: new_user}
|
|
361
432
|
end
|
|
362
433
|
|
|
434
|
+
def self.store_oauth_state_cookie(ctx, state)
|
|
435
|
+
return unless ctx.request
|
|
436
|
+
|
|
437
|
+
cookie = ctx.context.create_auth_cookie("state", max_age: 600)
|
|
438
|
+
ctx.set_signed_cookie(cookie.name, state, ctx.context.secret, cookie.attributes)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def self.valid_oauth_state_cookie?(ctx, state)
|
|
442
|
+
return true unless ctx.request
|
|
443
|
+
|
|
444
|
+
cookie = ctx.context.create_auth_cookie("state", max_age: 600)
|
|
445
|
+
stored = ctx.get_signed_cookie(cookie.name, ctx.context.secret)
|
|
446
|
+
Cookies.expire_cookie(ctx, cookie)
|
|
447
|
+
stored == state
|
|
448
|
+
end
|
|
449
|
+
|
|
363
450
|
def self.oauth_error_url(base_url, error, description = nil)
|
|
364
451
|
uri = URI.parse(base_url.to_s)
|
|
365
452
|
query = URI.decode_www_form(uri.query.to_s)
|
|
@@ -377,17 +464,32 @@ module BetterAuth
|
|
|
377
464
|
!!(fetch_value(provider, "disableSignUp") || fetch_value(fetch_value(provider, "options") || {}, "disableSignUp"))
|
|
378
465
|
end
|
|
379
466
|
|
|
380
|
-
def self.linkable_provider?(ctx, provider_id, user_info, implicit: false)
|
|
467
|
+
def self.linkable_provider?(ctx, provider_id, user_info, implicit: false, local_user: nil)
|
|
381
468
|
linking = ctx.context.options.account[:account_linking] || {}
|
|
382
469
|
return false if linking[:enabled] == false
|
|
383
470
|
return false if implicit && linking[:disable_implicit_linking] == true
|
|
471
|
+
if implicit && linking.fetch(:require_local_email_verified, true) && !local_user&.fetch("emailVerified", false)
|
|
472
|
+
return false
|
|
473
|
+
end
|
|
384
474
|
|
|
385
475
|
trusted = Array(linking[:trusted_providers]).map(&:to_s).include?(provider_id.to_s)
|
|
386
476
|
trusted || !!fetch_value(user_info, "emailVerified")
|
|
387
477
|
end
|
|
388
478
|
|
|
479
|
+
def self.implicit_link_allowed?(ctx, local_user)
|
|
480
|
+
linking = ctx.context.options.account[:account_linking] || {}
|
|
481
|
+
return false if linking[:enabled] == false || linking[:disable_implicit_linking] == true
|
|
482
|
+
|
|
483
|
+
linking[:require_local_email_verified] == false || !!local_user["emailVerified"]
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def self.blank_remote_id?(value)
|
|
487
|
+
value.nil? || value.to_s.strip.empty?
|
|
488
|
+
end
|
|
489
|
+
|
|
389
490
|
def self.link_social_account_from_callback(ctx, provider_id, user_info, tokens, link)
|
|
390
491
|
return {error: "unable_to_link_account"} unless linkable_provider?(ctx, provider_id, user_info)
|
|
492
|
+
return {error: "unable_to_link_account"} if blank_remote_id?(fetch_value(user_info, "id"))
|
|
391
493
|
|
|
392
494
|
email = fetch_value(user_info, "email").to_s.downcase
|
|
393
495
|
link_email = fetch_value(link, "email").to_s.downcase
|
|
@@ -403,20 +505,21 @@ module BetterAuth
|
|
|
403
505
|
"userId" => user_id
|
|
404
506
|
)
|
|
405
507
|
existing = ctx.context.internal_adapter.find_account_by_provider_id(account_id, provider_id)
|
|
406
|
-
|
|
407
|
-
|
|
508
|
+
begin
|
|
509
|
+
account = if existing
|
|
510
|
+
return {error: "account_already_linked_to_different_user"} if existing["userId"].to_s != user_id
|
|
408
511
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
512
|
+
ctx.context.internal_adapter.update_account(existing["id"], account_info)
|
|
513
|
+
else
|
|
514
|
+
ctx.context.internal_adapter.create_account(account_info)
|
|
515
|
+
end
|
|
516
|
+
raise BetterAuth::Error, "Unable to link social account" unless account
|
|
517
|
+
rescue
|
|
518
|
+
social_log(ctx.context, :error, "Unable to link social account")
|
|
519
|
+
return {error: "unable_to_link_account"}
|
|
412
520
|
end
|
|
413
521
|
|
|
414
|
-
|
|
415
|
-
ctx.context.internal_adapter.update_user(user_id, {
|
|
416
|
-
name: fetch_value(user_info, "name"),
|
|
417
|
-
image: fetch_value(user_info, "image")
|
|
418
|
-
}.compact)
|
|
419
|
-
end
|
|
522
|
+
update_social_user_info_on_link(ctx, user_id, user_info)
|
|
420
523
|
update_verified_email_on_link(ctx, user_id, link_email, user_info)
|
|
421
524
|
|
|
422
525
|
{status: true}
|
|
@@ -434,13 +537,33 @@ module BetterAuth
|
|
|
434
537
|
else
|
|
435
538
|
!!fetch_value(user_info, "emailVerified")
|
|
436
539
|
end
|
|
437
|
-
|
|
540
|
+
provider_fields = Schema.parse_provider_profile_user_input(ctx.context.options, user_info, action: :update)
|
|
541
|
+
update = provider_fields.merge(
|
|
438
542
|
"email" => email,
|
|
439
543
|
"name" => fetch_value(user_info, "name").to_s,
|
|
440
544
|
"image" => fetch_value(user_info, "image"),
|
|
441
545
|
"emailVerified" => email_verified
|
|
442
|
-
|
|
443
|
-
ctx.context.internal_adapter.update_user(user["id"], update)
|
|
546
|
+
).reject { |_key, value| value.nil? }
|
|
547
|
+
ctx.context.internal_adapter.update_user(user["id"], update)
|
|
548
|
+
rescue
|
|
549
|
+
social_log(ctx.context, :warn, "Could not override social user info")
|
|
550
|
+
raise
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def self.update_social_user_info_on_link(ctx, user_id, user_info)
|
|
554
|
+
return unless ctx.context.options.account.dig(:account_linking, :update_user_info_on_link)
|
|
555
|
+
|
|
556
|
+
provider_fields = Schema.parse_provider_profile_user_input(ctx.context.options, user_info, action: :update)
|
|
557
|
+
update = provider_fields.merge(
|
|
558
|
+
"name" => fetch_value(user_info, "name"),
|
|
559
|
+
"image" => fetch_value(user_info, "image")
|
|
560
|
+
).reject { |_key, value| value.nil? }
|
|
561
|
+
return if update.empty?
|
|
562
|
+
|
|
563
|
+
ctx.context.internal_adapter.update_user(user_id, update)
|
|
564
|
+
rescue
|
|
565
|
+
social_log(ctx.context, :warn, "Could not update user info on account link")
|
|
566
|
+
nil
|
|
444
567
|
end
|
|
445
568
|
|
|
446
569
|
def self.safe_additional_state(body)
|
|
@@ -479,6 +602,17 @@ module BetterAuth
|
|
|
479
602
|
ctx.context.internal_adapter.update_user(user_id, {"emailVerified" => true})
|
|
480
603
|
end
|
|
481
604
|
|
|
605
|
+
def self.promote_verified_email_on_link!(ctx, local_user, social_user)
|
|
606
|
+
return if local_user["emailVerified"] == true
|
|
607
|
+
return unless fetch_value(social_user, "emailVerified")
|
|
608
|
+
return unless fetch_value(social_user, "email").to_s.strip.downcase == local_user["email"].to_s.strip.downcase
|
|
609
|
+
|
|
610
|
+
updated = ctx.context.internal_adapter.update_user(local_user.fetch("id"), {"emailVerified" => true})
|
|
611
|
+
raise BetterAuth::Error, "Failed to promote verified email" unless updated
|
|
612
|
+
|
|
613
|
+
updated
|
|
614
|
+
end
|
|
615
|
+
|
|
482
616
|
def self.parse_json_hash(value)
|
|
483
617
|
return value if value.is_a?(Hash)
|
|
484
618
|
return {} if value.nil? || value.to_s.empty?
|
|
@@ -488,5 +622,14 @@ module BetterAuth
|
|
|
488
622
|
rescue JSON::ParserError
|
|
489
623
|
{}
|
|
490
624
|
end
|
|
625
|
+
|
|
626
|
+
def self.social_log(context, level, message)
|
|
627
|
+
logger = context.logger
|
|
628
|
+
if logger.respond_to?(:call)
|
|
629
|
+
logger.call(level, message)
|
|
630
|
+
elsif logger.respond_to?(level)
|
|
631
|
+
logger.public_send(level, message)
|
|
632
|
+
end
|
|
633
|
+
end
|
|
491
634
|
end
|
|
492
635
|
end
|
|
@@ -80,14 +80,16 @@ module BetterAuth
|
|
|
80
80
|
current_password = body["currentPassword"] || body["current_password"]
|
|
81
81
|
validate_password_length!(new_password, ctx.context.options.email_and_password)
|
|
82
82
|
account = credential_account(ctx, session[:user]["id"])
|
|
83
|
-
|
|
83
|
+
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["CREDENTIAL_ACCOUNT_NOT_FOUND"]) unless account && account["password"]
|
|
84
|
+
|
|
85
|
+
unless verify_password_value(ctx, current_password.to_s, account["password"])
|
|
84
86
|
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"])
|
|
85
87
|
end
|
|
86
88
|
|
|
87
89
|
ctx.context.internal_adapter.update_account(account["id"], password: hash_password(ctx, new_password))
|
|
88
90
|
token = nil
|
|
89
91
|
if body["revokeOtherSessions"] || body["revoke_other_sessions"]
|
|
90
|
-
ctx.context.internal_adapter.
|
|
92
|
+
ctx.context.internal_adapter.delete_user_sessions(session[:user]["id"])
|
|
91
93
|
new_session = ctx.context.internal_adapter.create_session(session[:user]["id"])
|
|
92
94
|
Cookies.set_session_cookie(ctx, {session: new_session, user: session[:user]})
|
|
93
95
|
token = new_session["token"]
|
|
@@ -174,9 +176,12 @@ module BetterAuth
|
|
|
174
176
|
session = current_session(ctx, sensitive: true)
|
|
175
177
|
body = normalize_hash(ctx.body)
|
|
176
178
|
sender = ctx.context.options.user.dig(:delete_user, :send_delete_account_verification)
|
|
179
|
+
token_link_base_url = ctx.context.token_link_base_url if !body["token"] && sender.respond_to?(:call)
|
|
177
180
|
if body["password"]
|
|
178
181
|
account = credential_account(ctx, session[:user]["id"])
|
|
179
|
-
|
|
182
|
+
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["CREDENTIAL_ACCOUNT_NOT_FOUND"]) unless account && account["password"]
|
|
183
|
+
|
|
184
|
+
unless verify_password_value(ctx, body["password"], account["password"])
|
|
180
185
|
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"])
|
|
181
186
|
end
|
|
182
187
|
end
|
|
@@ -187,7 +192,7 @@ module BetterAuth
|
|
|
187
192
|
token = SecureRandom.hex(16)
|
|
188
193
|
expires_in = ctx.context.options.user.dig(:delete_user, :delete_token_expires_in) || 3600
|
|
189
194
|
callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"] || "/"
|
|
190
|
-
url = "#{
|
|
195
|
+
url = "#{token_link_base_url}/delete-user/callback?token=#{URI.encode_www_form_component(token)}&callbackURL=#{URI.encode_www_form_component(callback_url)}"
|
|
191
196
|
ctx.context.internal_adapter.create_verification_value(
|
|
192
197
|
identifier: "delete-account-#{token}",
|
|
193
198
|
value: session[:user]["id"],
|
|
@@ -294,7 +299,7 @@ module BetterAuth
|
|
|
294
299
|
}
|
|
295
300
|
) do |ctx|
|
|
296
301
|
enabled = ctx.context.options.user.dig(:change_email, :enabled)
|
|
297
|
-
raise APIError.new("BAD_REQUEST",
|
|
302
|
+
raise APIError.new("BAD_REQUEST", code: "CHANGE_EMAIL_DISABLED", message: BASE_ERROR_CODES["CHANGE_EMAIL_DISABLED"]) unless enabled
|
|
298
303
|
session = current_session(ctx, sensitive: true)
|
|
299
304
|
body = normalize_hash(ctx.body)
|
|
300
305
|
new_email = (body["newEmail"] || body["new_email"]).to_s.downcase
|
|
@@ -308,6 +313,7 @@ module BetterAuth
|
|
|
308
313
|
unless can_update_without_verification || can_send_confirmation || can_send_verification
|
|
309
314
|
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VERIFICATION_EMAIL_NOT_ENABLED"])
|
|
310
315
|
end
|
|
316
|
+
ctx.context.token_link_base_url if can_send_confirmation || can_send_verification
|
|
311
317
|
|
|
312
318
|
existing_target = ctx.context.internal_adapter.find_user_by_email(new_email)
|
|
313
319
|
next ctx.json({status: true}) if existing_target
|
|
@@ -322,6 +328,7 @@ module BetterAuth
|
|
|
322
328
|
if can_send_confirmation
|
|
323
329
|
callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"]
|
|
324
330
|
token = create_email_verification_token(ctx, session[:user]["email"], update_to: new_email, extra: {"requestType" => "change-email-confirmation"})
|
|
331
|
+
register_change_email_token!(ctx, token)
|
|
325
332
|
url = email_verification_url(ctx, token, callback_url)
|
|
326
333
|
confirmation_sender.call({user: session[:user], new_email: new_email, url: url, token: token}, ctx.request)
|
|
327
334
|
next ctx.json({status: true})
|
|
@@ -334,20 +341,20 @@ module BetterAuth
|
|
|
334
341
|
|
|
335
342
|
def self.send_change_email_verification(ctx, sender, user, current_email, new_email, callback_url)
|
|
336
343
|
token = create_email_verification_token(ctx, current_email, update_to: new_email, extra: {"requestType" => "change-email-verification"})
|
|
344
|
+
register_change_email_token!(ctx, token)
|
|
337
345
|
sender.call({user: user.merge("email" => new_email), url: email_verification_url(ctx, token, callback_url), token: token}, ctx.request)
|
|
338
346
|
end
|
|
339
347
|
|
|
340
348
|
def self.email_verification_url(ctx, token, callback_url)
|
|
341
349
|
callback = URI.encode_www_form_component(callback_url || "/")
|
|
342
|
-
"#{ctx.context.
|
|
350
|
+
"#{ctx.context.token_link_base_url}/verify-email?token=#{URI.encode_www_form_component(token)}&callbackURL=#{callback}"
|
|
343
351
|
end
|
|
344
352
|
|
|
345
353
|
def self.delete_user_by_token!(ctx, session, token)
|
|
346
|
-
verification = ctx.context.internal_adapter.
|
|
347
|
-
unless verification && verification["value"] == session[:user]["id"]
|
|
354
|
+
verification = ctx.context.internal_adapter.consume_verification_value("delete-account-#{token}")
|
|
355
|
+
unless verification && verification["value"] == session[:user]["id"]
|
|
348
356
|
raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES["INVALID_TOKEN"])
|
|
349
357
|
end
|
|
350
|
-
ctx.context.internal_adapter.delete_verification_value(verification["id"])
|
|
351
358
|
end
|
|
352
359
|
|
|
353
360
|
def self.delete_current_user!(ctx, session)
|
|
@@ -356,7 +363,7 @@ module BetterAuth
|
|
|
356
363
|
deleted = ctx.context.internal_adapter.delete_user(session[:user]["id"])
|
|
357
364
|
raise APIError.new("BAD_REQUEST", message: "User delete aborted") if deleted == false
|
|
358
365
|
|
|
359
|
-
ctx.context.internal_adapter.
|
|
366
|
+
ctx.context.internal_adapter.delete_user_sessions(session[:user]["id"])
|
|
360
367
|
Cookies.delete_session_cookie(ctx)
|
|
361
368
|
call_option(config[:after_delete], session[:user], ctx.request)
|
|
362
369
|
end
|