better_auth 0.10.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 +83 -0
- data/LICENSE.md +1 -1
- data/README.md +138 -91
- 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 +191 -56
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +294 -49
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +41 -46
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +110 -14
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +14 -3
- data/lib/better_auth/error.rb +3 -1
- 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 +4 -4
- 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 +79 -28
- 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 +47 -9
- data/lib/better_auth/plugins/dub.rb +2 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +141 -146
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +37 -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 +142 -32
- 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 +61 -60
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +35 -7
- data/lib/better_auth/plugins/organization.rb +455 -122
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +24 -26
- 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 +186 -21
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +297 -135
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +40 -8
- data/lib/better_auth/routes/email_verification.rb +88 -5
- data/lib/better_auth/routes/password.rb +5 -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 +169 -54
- data/lib/better_auth/routes/user.rb +11 -8
- data/lib/better_auth/schema/sql.rb +90 -24
- data/lib/better_auth/schema.rb +124 -3
- data/lib/better_auth/session.rb +27 -9
- 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 +41 -14
- 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/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 +5 -5
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +11 -41
- metadata +5 -13
- 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 -256
- data/lib/better_auth/plugins/oidc_provider.rb +0 -769
|
@@ -31,14 +31,14 @@ module BetterAuth
|
|
|
31
31
|
init: ->(context) {
|
|
32
32
|
{
|
|
33
33
|
options: {
|
|
34
|
-
social_providers: generic_oauth_social_providers(config, context)
|
|
34
|
+
social_providers: context.social_providers.merge(generic_oauth_social_providers(config, context))
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
endpoints: {
|
|
39
39
|
sign_in_with_oauth2: sign_in_with_oauth2_endpoint(config),
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
oauth2_callback: oauth2_callback_endpoint(config),
|
|
41
|
+
oauth2_link_account: oauth2_link_account_endpoint(config)
|
|
42
42
|
},
|
|
43
43
|
error_codes: GENERIC_OAUTH_ERROR_CODES,
|
|
44
44
|
options: config
|
|
@@ -212,6 +212,36 @@ module BetterAuth
|
|
|
212
212
|
)
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
+
def yandex(options = {})
|
|
216
|
+
data = normalize_hash(options)
|
|
217
|
+
generic_oauth_provider_config(
|
|
218
|
+
data,
|
|
219
|
+
provider_id: "yandex",
|
|
220
|
+
authorization_url: "https://oauth.yandex.com/authorize",
|
|
221
|
+
token_url: "https://oauth.yandex.com/token",
|
|
222
|
+
scopes: ["login:info", "login:email", "login:avatar"],
|
|
223
|
+
get_user_info: ->(tokens) {
|
|
224
|
+
profile = generic_oauth_fetch_json(
|
|
225
|
+
"https://login.yandex.ru/info?format=json",
|
|
226
|
+
authorization: "OAuth #{fetch_value(tokens, "accessToken")}"
|
|
227
|
+
)
|
|
228
|
+
return nil unless profile
|
|
229
|
+
|
|
230
|
+
avatar_id = fetch_value(profile, "default_avatar_id")
|
|
231
|
+
image = if !fetch_value(profile, "is_avatar_empty") && !avatar_id.to_s.empty?
|
|
232
|
+
"https://avatars.yandex.net/get-yapic/#{avatar_id}/islands-200"
|
|
233
|
+
end
|
|
234
|
+
{
|
|
235
|
+
id: fetch_value(profile, "id"),
|
|
236
|
+
name: fetch_value(profile, "display_name") || fetch_value(profile, "real_name") || fetch_value(profile, "first_name") || fetch_value(profile, "login"),
|
|
237
|
+
email: fetch_value(profile, "default_email") || Array(fetch_value(profile, "emails")).first,
|
|
238
|
+
emailVerified: false,
|
|
239
|
+
image: image
|
|
240
|
+
}.compact
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
215
245
|
def sign_in_with_oauth2_endpoint(config)
|
|
216
246
|
Endpoint.new(
|
|
217
247
|
path: "/sign-in/oauth2",
|
|
@@ -235,7 +265,7 @@ module BetterAuth
|
|
|
235
265
|
end
|
|
236
266
|
end
|
|
237
267
|
|
|
238
|
-
def
|
|
268
|
+
def oauth2_link_account_endpoint(config)
|
|
239
269
|
Endpoint.new(
|
|
240
270
|
path: "/oauth2/link",
|
|
241
271
|
method: "POST",
|
|
@@ -266,7 +296,7 @@ module BetterAuth
|
|
|
266
296
|
end
|
|
267
297
|
end
|
|
268
298
|
|
|
269
|
-
def
|
|
299
|
+
def oauth2_callback_endpoint(config)
|
|
270
300
|
Endpoint.new(
|
|
271
301
|
path: "/oauth2/callback/:providerId",
|
|
272
302
|
method: "GET",
|
|
@@ -291,9 +321,9 @@ module BetterAuth
|
|
|
291
321
|
provider = generic_oauth_provider!(config, provider_id)
|
|
292
322
|
state_data = generic_oauth_parse_state(ctx, query[:state].to_s)
|
|
293
323
|
error_url = state_data["errorURL"] || state_data["errorCallbackURL"] || "#{ctx.context.base_url}/error"
|
|
294
|
-
redirect_error = ->(error) { raise ctx.redirect(generic_oauth_error_url(error_url, error)) }
|
|
324
|
+
redirect_error = ->(error, description = nil) { raise ctx.redirect(generic_oauth_error_url(error_url, error, description)) }
|
|
295
325
|
|
|
296
|
-
redirect_error.call(query[:error] || "oAuth_code_missing") if query[:error] || query[:code].to_s.empty?
|
|
326
|
+
redirect_error.call(query[:error] || "oAuth_code_missing", query[:error_description]) if query[:error] || query[:code].to_s.empty?
|
|
297
327
|
generic_oauth_validate_issuer!(ctx, provider, query, redirect_error)
|
|
298
328
|
|
|
299
329
|
tokens = begin
|
|
@@ -311,6 +341,7 @@ module BetterAuth
|
|
|
311
341
|
account_id = fetch_value(mapped_user, "id").to_s
|
|
312
342
|
redirect_error.call("email_is_missing") if email.empty?
|
|
313
343
|
redirect_error.call("name_is_missing") if name.empty?
|
|
344
|
+
redirect_error.call("user_info_is_missing") if Routes.blank_remote_id?(account_id)
|
|
314
345
|
|
|
315
346
|
link = state_data["link"]
|
|
316
347
|
callback_url = state_data["callbackURL"] || "/"
|
|
@@ -323,21 +354,20 @@ module BetterAuth
|
|
|
323
354
|
if !existing && (provider[:disable_sign_up] || (provider[:disable_implicit_sign_up] && !state_data["requestSignUp"]))
|
|
324
355
|
redirect_error.call("signup_disabled")
|
|
325
356
|
end
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
"
|
|
331
|
-
|
|
357
|
+
session_data = begin
|
|
358
|
+
Routes.persist_social_user(
|
|
359
|
+
ctx,
|
|
360
|
+
provider_id,
|
|
361
|
+
mapped_user.merge("email" => email, "name" => name, "id" => account_id),
|
|
362
|
+
generic_oauth_account_info(ctx, provider_id, account_id, tokens),
|
|
363
|
+
override_user_info: provider[:override_user_info]
|
|
332
364
|
)
|
|
333
|
-
|
|
365
|
+
rescue APIError => error
|
|
366
|
+
raise if error.code.to_s.empty?
|
|
334
367
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
mapped_user.merge("email" => email, "name" => name, "id" => account_id),
|
|
339
|
-
generic_oauth_account_info(ctx, provider_id, account_id, tokens)
|
|
340
|
-
)
|
|
368
|
+
redirect_error.call(error.code, error.message)
|
|
369
|
+
end
|
|
370
|
+
redirect_error.call(session_data[:error].tr(" ", "_")) if session_data[:error]
|
|
341
371
|
generic_oauth_set_account_cookie(ctx, provider_id, account_id, session_data[:user]["id"])
|
|
342
372
|
Cookies.set_session_cookie(ctx, session_data)
|
|
343
373
|
raise ctx.redirect(existing ? callback_url : (state_data["newUserURL"] || state_data["newUserCallbackURL"] || callback_url))
|
|
@@ -358,31 +388,31 @@ module BetterAuth
|
|
|
358
388
|
OpenAPI.object_schema(
|
|
359
389
|
{
|
|
360
390
|
providerId: {type: "string", description: "OAuth provider ID"},
|
|
361
|
-
provider_id: {type: "string", description: "OAuth provider ID"},
|
|
362
391
|
callbackURL: {type: "string", description: "URL to redirect to after success"},
|
|
363
|
-
callback_url: {type: "string", description: "URL to redirect to after success"},
|
|
364
392
|
errorCallbackURL: {type: "string", description: "URL to redirect to after error"},
|
|
365
|
-
error_callback_url: {type: "string", description: "URL to redirect to after error"},
|
|
366
393
|
newUserCallbackURL: {type: "string", description: "URL to redirect to for new users"},
|
|
367
|
-
new_user_callback_url: {type: "string", description: "URL to redirect to for new users"},
|
|
368
394
|
requestSignUp: {type: "boolean", description: "Whether this request is a sign-up flow"},
|
|
369
|
-
request_sign_up: {type: "boolean", description: "Whether this request is a sign-up flow"},
|
|
370
395
|
scopes: {type: "array", items: {type: "string"}, description: "Additional OAuth scopes"},
|
|
371
396
|
disableRedirect: {type: "boolean", description: "Return the URL instead of redirecting"},
|
|
372
|
-
|
|
373
|
-
additionalData: {type: "object", additionalProperties: true},
|
|
374
|
-
additional_data: {type: "object", additionalProperties: true}
|
|
397
|
+
additionalData: {type: "object", additionalProperties: true}
|
|
375
398
|
}
|
|
376
399
|
)
|
|
377
400
|
end
|
|
378
401
|
|
|
379
|
-
def generic_oauth_authorization_url(ctx, provider, body, link:)
|
|
402
|
+
def generic_oauth_authorization_url(ctx, provider, body, link:, code_verifier: nil, preserve_additional_data: false)
|
|
380
403
|
authorization_url = provider[:authorization_url] || generic_oauth_discovery(provider)["authorization_endpoint"]
|
|
381
404
|
token_url = provider[:token_url] || generic_oauth_discovery(provider)["token_endpoint"]
|
|
382
405
|
raise APIError.new("BAD_REQUEST", message: GENERIC_OAUTH_ERROR_CODES["INVALID_OAUTH_CONFIGURATION"]) if authorization_url.to_s.empty? || token_url.to_s.empty?
|
|
383
406
|
|
|
384
|
-
code_verifier
|
|
385
|
-
|
|
407
|
+
code_verifier ||= Crypto.random_string(43)
|
|
408
|
+
additional_data = if preserve_additional_data
|
|
409
|
+
raw = body[:additional_data] || body[:additionalData]
|
|
410
|
+
raw.is_a?(Hash) ? raw.each_with_object({}) { |(key, value), data| data[key.to_s] = value } : {}
|
|
411
|
+
else
|
|
412
|
+
normalize_hash(body[:additional_data] || body[:additionalData]).transform_keys(&:to_s)
|
|
413
|
+
end
|
|
414
|
+
additional_data = additional_data.reject { |key, _value| OAuthState::INTERNAL_KEYS.include?(key.to_s) }
|
|
415
|
+
state_data = additional_data.merge(
|
|
386
416
|
"callbackURL" => body[:callback_url] || body[:callbackURL] || "/",
|
|
387
417
|
"errorURL" => body[:error_callback_url] || body[:errorCallbackURL],
|
|
388
418
|
"newUserURL" => body[:new_user_callback_url] || body[:newUserCallbackURL],
|
|
@@ -392,79 +422,59 @@ module BetterAuth
|
|
|
392
422
|
"expiresAt" => Time.now.to_i + 600
|
|
393
423
|
)
|
|
394
424
|
state = generic_oauth_generate_state(ctx, state_data)
|
|
395
|
-
legacy_state = Crypto.sign_jwt(
|
|
396
|
-
{
|
|
397
|
-
"callbackURL" => body[:callback_url] || body[:callbackURL] || "/",
|
|
398
|
-
"errorURL" => body[:error_callback_url] || body[:errorCallbackURL],
|
|
399
|
-
"newUserURL" => body[:new_user_callback_url] || body[:newUserCallbackURL],
|
|
400
|
-
"requestSignUp" => body[:request_sign_up] || body[:requestSignUp],
|
|
401
|
-
"codeVerifier" => code_verifier,
|
|
402
|
-
"link" => link
|
|
403
|
-
},
|
|
404
|
-
ctx.context.secret,
|
|
405
|
-
expires_in: 600
|
|
406
|
-
)
|
|
407
|
-
state ||= legacy_state
|
|
408
425
|
|
|
409
426
|
uri = URI.parse(authorization_url.to_s)
|
|
410
427
|
params = URI.decode_www_form(uri.query.to_s)
|
|
411
|
-
params.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
["state", state]
|
|
416
|
-
])
|
|
428
|
+
generic_oauth_set_query_param(params, "client_id", provider[:client_id].to_s)
|
|
429
|
+
generic_oauth_set_query_param(params, "response_type", provider[:response_type] || "code")
|
|
430
|
+
generic_oauth_set_query_param(params, "redirect_uri", generic_oauth_redirect_uri(ctx, provider))
|
|
431
|
+
generic_oauth_set_query_param(params, "state", state)
|
|
417
432
|
scopes = Array(body[:scopes]) + Array(provider[:scopes])
|
|
418
|
-
params
|
|
433
|
+
generic_oauth_set_query_param(params, "scope", scopes.join(" ")) unless scopes.empty?
|
|
419
434
|
if provider[:pkce]
|
|
420
|
-
params
|
|
421
|
-
params
|
|
435
|
+
generic_oauth_set_query_param(params, "code_challenge", generic_oauth_pkce_challenge(code_verifier))
|
|
436
|
+
generic_oauth_set_query_param(params, "code_challenge_method", "S256")
|
|
422
437
|
end
|
|
423
|
-
params
|
|
424
|
-
params
|
|
425
|
-
params
|
|
438
|
+
generic_oauth_set_query_param(params, "prompt", provider[:prompt]) if provider[:prompt]
|
|
439
|
+
generic_oauth_set_query_param(params, "access_type", provider[:access_type]) if provider[:access_type]
|
|
440
|
+
generic_oauth_set_query_param(params, "response_mode", provider[:response_mode]) if provider[:response_mode]
|
|
426
441
|
authorization_params = if provider[:authorization_url_params].respond_to?(:call)
|
|
427
442
|
provider[:authorization_url_params].call(ctx)
|
|
428
443
|
else
|
|
429
444
|
provider[:authorization_url_params]
|
|
430
445
|
end
|
|
431
|
-
normalize_hash(authorization_params || {}).each { |key, value| params
|
|
446
|
+
normalize_hash(authorization_params || {}).each { |key, value| generic_oauth_set_query_param(params, key, value) }
|
|
432
447
|
uri.query = URI.encode_www_form(params)
|
|
433
448
|
uri.to_s
|
|
434
449
|
end
|
|
435
450
|
|
|
436
|
-
def
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
451
|
+
def generic_oauth_set_query_param(params, key, value)
|
|
452
|
+
name = key.to_s
|
|
453
|
+
first_index = params.index { |param_name, _param_value| param_name == name }
|
|
454
|
+
if first_index
|
|
455
|
+
params[first_index] = [name, value.to_s]
|
|
456
|
+
(params.length - 1).downto(first_index + 1) do |index|
|
|
457
|
+
params.delete_at(index) if params[index].first == name
|
|
458
|
+
end
|
|
459
|
+
else
|
|
460
|
+
params << [name, value.to_s]
|
|
444
461
|
end
|
|
462
|
+
end
|
|
445
463
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
ctx.context.internal_adapter.create_verification_value(
|
|
449
|
-
identifier: state,
|
|
450
|
-
value: JSON.generate(state_data),
|
|
451
|
-
expiresAt: Time.now + 600
|
|
452
|
-
)
|
|
453
|
-
state
|
|
454
|
-
rescue
|
|
455
|
-
nil
|
|
464
|
+
def generic_oauth_generate_state(ctx, state_data)
|
|
465
|
+
OAuthState.generate(ctx, state_data)
|
|
456
466
|
end
|
|
457
467
|
|
|
458
468
|
def generic_oauth_exchange_token(ctx, provider, code, state_data)
|
|
459
469
|
token_callback = provider[:get_token]
|
|
460
470
|
if token_callback.respond_to?(:call)
|
|
461
|
-
return
|
|
471
|
+
return generic_oauth_normalize_tokens(token_callback.call(
|
|
462
472
|
code: code,
|
|
463
473
|
redirectURI: generic_oauth_redirect_uri(ctx, provider),
|
|
464
474
|
redirect_uri: generic_oauth_redirect_uri(ctx, provider),
|
|
465
475
|
codeVerifier: provider[:pkce] ? state_data["codeVerifier"] : nil,
|
|
466
476
|
code_verifier: provider[:pkce] ? state_data["codeVerifier"] : nil
|
|
467
|
-
))
|
|
477
|
+
), access_token_expires_in: provider[:access_token_expires_in])
|
|
468
478
|
end
|
|
469
479
|
|
|
470
480
|
token_url = provider[:token_url] || generic_oauth_discovery(provider)["token_endpoint"]
|
|
@@ -474,73 +484,37 @@ module BetterAuth
|
|
|
474
484
|
end
|
|
475
485
|
|
|
476
486
|
def generic_oauth_parse_state(ctx, state)
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
cookie = ctx.context.create_auth_cookie("oauth_state")
|
|
483
|
-
encrypted = ctx.get_cookie(cookie.name)
|
|
484
|
-
unless encrypted
|
|
485
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "state_mismatch"))
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
begin
|
|
489
|
-
decrypted = Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: encrypted)
|
|
490
|
-
unless decrypted
|
|
491
|
-
Cookies.expire_cookie(ctx, cookie)
|
|
492
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "please_restart_the_process"))
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
parsed = JSON.parse(decrypted)
|
|
496
|
-
rescue JSON::ParserError
|
|
497
|
-
Cookies.expire_cookie(ctx, cookie)
|
|
498
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "please_restart_the_process"))
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
Cookies.expire_cookie(ctx, cookie)
|
|
502
|
-
if parsed["state"] != state
|
|
503
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "state_mismatch"))
|
|
504
|
-
end
|
|
505
|
-
if parsed["expiresAt"].to_i.positive? && parsed["expiresAt"].to_i < Time.now.to_i
|
|
506
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "state_mismatch"))
|
|
507
|
-
end
|
|
508
|
-
return parsed
|
|
509
|
-
else
|
|
510
|
-
verification = ctx.context.internal_adapter.find_verification_value(state)
|
|
511
|
-
if verification
|
|
512
|
-
cookie = ctx.context.create_auth_cookie("state")
|
|
513
|
-
cookie_state = ctx.get_signed_cookie(cookie.name, ctx.context.secret)
|
|
514
|
-
if ctx.request && cookie_state != state
|
|
515
|
-
Cookies.expire_cookie(ctx, cookie)
|
|
516
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "state_mismatch"))
|
|
517
|
-
elsif !ctx.request && cookie_state && cookie_state != state
|
|
518
|
-
Cookies.expire_cookie(ctx, cookie)
|
|
519
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "state_mismatch"))
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
parsed = JSON.parse(verification.fetch("value"))
|
|
523
|
-
ctx.context.internal_adapter.delete_verification_value(verification.fetch("id"))
|
|
524
|
-
Cookies.expire_cookie(ctx, cookie) if ctx.request || cookie_state
|
|
525
|
-
return parsed
|
|
526
|
-
end
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
Crypto.verify_jwt(state.to_s, ctx.context.secret) || {}
|
|
530
|
-
rescue JSON::ParserError
|
|
531
|
-
{}
|
|
487
|
+
OAuthState.parse(ctx, state)
|
|
488
|
+
rescue OAuthState::Error => error
|
|
489
|
+
code = (error.code == "state_not_found") ? "please_restart_the_process" : error.code
|
|
490
|
+
error_url = error.error_url || generic_oauth_state_error_url(ctx)
|
|
491
|
+
raise ctx.redirect(generic_oauth_error_url(error_url, code))
|
|
532
492
|
end
|
|
533
493
|
|
|
534
494
|
def generic_oauth_state_error_url(ctx)
|
|
535
495
|
ctx.context.options.on_api_error[:error_url] || "#{ctx.context.base_url}/error"
|
|
536
496
|
end
|
|
537
497
|
|
|
498
|
+
def generic_oauth_recovered_state_error_url(ctx, state_data)
|
|
499
|
+
state_data["errorURL"] || state_data["errorCallbackURL"] || generic_oauth_state_error_url(ctx)
|
|
500
|
+
end
|
|
501
|
+
|
|
538
502
|
def generic_oauth_user_info(provider, tokens)
|
|
539
503
|
callback = provider[:get_user_info]
|
|
540
|
-
|
|
504
|
+
if callback.respond_to?(:call)
|
|
505
|
+
user_info = callback.call(tokens)
|
|
506
|
+
return nil if user_info.nil?
|
|
507
|
+
|
|
508
|
+
return normalize_hash(user_info)
|
|
509
|
+
end
|
|
541
510
|
|
|
542
511
|
id_token = tokens[:id_token] || tokens[:idToken]
|
|
543
|
-
|
|
512
|
+
if id_token
|
|
513
|
+
token_user = generic_oauth_user_from_id_token(id_token)
|
|
514
|
+
id = fetch_value(token_user, "id")
|
|
515
|
+
email = fetch_value(token_user, "email")
|
|
516
|
+
return token_user if !id.to_s.empty? && !email.to_s.empty?
|
|
517
|
+
end
|
|
544
518
|
|
|
545
519
|
user_info_url = provider[:user_info_url] || generic_oauth_discovery(provider)["userinfo_endpoint"]
|
|
546
520
|
return nil if user_info_url.to_s.empty?
|
|
@@ -620,7 +594,7 @@ module BetterAuth
|
|
|
620
594
|
end
|
|
621
595
|
|
|
622
596
|
def generic_oauth_redirect_uri(ctx, provider)
|
|
623
|
-
provider[:redirect_uri] || provider[:redirectURI] || "#{ctx.context.
|
|
597
|
+
provider[:redirect_uri] || provider[:redirectURI] || "#{ctx.context.canonical_base_url}/oauth2/callback/#{provider[:provider_id]}"
|
|
624
598
|
end
|
|
625
599
|
|
|
626
600
|
def generic_oauth_validate_issuer!(ctx, provider, query, redirect_error)
|
|
@@ -679,13 +653,13 @@ module BetterAuth
|
|
|
679
653
|
response = HTTPClient.request(uri, request)
|
|
680
654
|
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
681
655
|
|
|
682
|
-
generic_oauth_normalize_tokens(JSON.parse(response.body))
|
|
656
|
+
generic_oauth_normalize_tokens(JSON.parse(response.body), access_token_expires_in: provider[:access_token_expires_in])
|
|
683
657
|
rescue
|
|
684
658
|
nil
|
|
685
659
|
end
|
|
686
660
|
|
|
687
661
|
def generic_oauth_user_from_id_token(id_token)
|
|
688
|
-
payload = JWT.decode(id_token, nil, false).first
|
|
662
|
+
payload = ::JWT.decode(id_token, nil, false).first
|
|
689
663
|
normalize_hash(
|
|
690
664
|
id: payload["sub"],
|
|
691
665
|
email: payload["email"],
|
|
@@ -697,15 +671,17 @@ module BetterAuth
|
|
|
697
671
|
nil
|
|
698
672
|
end
|
|
699
673
|
|
|
700
|
-
def generic_oauth_normalize_tokens(data)
|
|
674
|
+
def generic_oauth_normalize_tokens(data, access_token_expires_in: nil)
|
|
701
675
|
token_data = normalize_hash(data)
|
|
676
|
+
access_token_expires_at = token_data[:access_token_expires_at] || generic_oauth_expiry_time(token_data[:expires_in])
|
|
677
|
+
access_token_expires_at ||= generic_oauth_expiry_time(access_token_expires_in)
|
|
702
678
|
token_data.merge(
|
|
703
679
|
access_token: token_data[:access_token],
|
|
704
680
|
refresh_token: token_data[:refresh_token],
|
|
705
681
|
id_token: token_data[:id_token],
|
|
706
|
-
access_token_expires_at:
|
|
707
|
-
refresh_token_expires_at: generic_oauth_expiry_time(token_data[:refresh_token_expires_in]),
|
|
708
|
-
scopes: generic_oauth_token_scopes(token_data[:scope]),
|
|
682
|
+
access_token_expires_at: access_token_expires_at,
|
|
683
|
+
refresh_token_expires_at: token_data[:refresh_token_expires_at] || generic_oauth_expiry_time(token_data[:refresh_token_expires_in]),
|
|
684
|
+
scopes: generic_oauth_token_scopes(token_data[:scopes] || token_data[:scope]),
|
|
709
685
|
raw: token_data
|
|
710
686
|
).compact
|
|
711
687
|
end
|
|
@@ -767,7 +743,8 @@ module BetterAuth
|
|
|
767
743
|
pkce: data[:pkce],
|
|
768
744
|
disable_implicit_sign_up: data[:disable_implicit_sign_up],
|
|
769
745
|
disable_sign_up: data[:disable_sign_up],
|
|
770
|
-
override_user_info: data[:override_user_info]
|
|
746
|
+
override_user_info: data[:override_user_info],
|
|
747
|
+
access_token_expires_in: data[:access_token_expires_in]
|
|
771
748
|
)
|
|
772
749
|
config[:scopes] = data[:scopes] if data[:scopes]
|
|
773
750
|
config.compact
|
|
@@ -780,7 +757,20 @@ module BetterAuth
|
|
|
780
757
|
id: provider_id,
|
|
781
758
|
name: provider_id,
|
|
782
759
|
get_user_info: ->(tokens) { generic_oauth_provider_user_info(provider, tokens) },
|
|
783
|
-
refresh_access_token: ->(refresh_token) { generic_oauth_refresh_access_token(context, provider, refresh_token) }
|
|
760
|
+
refresh_access_token: ->(refresh_token) { generic_oauth_refresh_access_token(context, provider, refresh_token) },
|
|
761
|
+
oauth_popup_authorization_url: lambda do |ctx, data|
|
|
762
|
+
additional_data = data[:additionalData] || data["additionalData"] || data[:additional_data] || data["additional_data"]
|
|
763
|
+
normalized = normalize_hash(data)
|
|
764
|
+
normalized[:additional_data] = additional_data if additional_data.is_a?(Hash)
|
|
765
|
+
generic_oauth_authorization_url(
|
|
766
|
+
ctx,
|
|
767
|
+
provider,
|
|
768
|
+
normalized,
|
|
769
|
+
link: nil,
|
|
770
|
+
code_verifier: normalized[:code_verifier],
|
|
771
|
+
preserve_additional_data: true
|
|
772
|
+
)
|
|
773
|
+
end
|
|
784
774
|
}
|
|
785
775
|
end
|
|
786
776
|
end
|
|
@@ -820,13 +810,18 @@ module BetterAuth
|
|
|
820
810
|
response = HTTPClient.request(uri, request)
|
|
821
811
|
raise APIError.new("BAD_REQUEST", message: GENERIC_OAUTH_ERROR_CODES["INVALID_OAUTH_CONFIG"]) unless response.is_a?(Net::HTTPSuccess)
|
|
822
812
|
|
|
823
|
-
generic_oauth_normalize_tokens(JSON.parse(response.body))
|
|
813
|
+
generic_oauth_normalize_tokens(JSON.parse(response.body), access_token_expires_in: provider[:access_token_expires_in])
|
|
824
814
|
end
|
|
825
815
|
|
|
826
|
-
def generic_oauth_error_url(base_url, error)
|
|
816
|
+
def generic_oauth_error_url(base_url, error, description = nil)
|
|
827
817
|
uri = URI.parse(base_url.to_s)
|
|
828
818
|
query = URI.decode_www_form(uri.query.to_s)
|
|
829
|
-
query
|
|
819
|
+
generic_oauth_set_query_param(query, "error", error)
|
|
820
|
+
if description.to_s.empty?
|
|
821
|
+
query.delete_if { |name, _value| name == "error_description" }
|
|
822
|
+
else
|
|
823
|
+
generic_oauth_set_query_param(query, "error_description", description)
|
|
824
|
+
end
|
|
830
825
|
uri.query = URI.encode_www_form(query)
|
|
831
826
|
uri.to_s
|
|
832
827
|
end
|