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
|
@@ -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",
|
|
@@ -220,6 +250,7 @@ module BetterAuth
|
|
|
220
250
|
openapi: {
|
|
221
251
|
operationId: "signInOAuth2",
|
|
222
252
|
description: "Sign in with OAuth2",
|
|
253
|
+
requestBody: OpenAPI.json_request_body(generic_oauth_start_body_schema),
|
|
223
254
|
responses: {
|
|
224
255
|
"200" => OpenAPI.json_response("Sign in with OAuth2", generic_oauth_url_response_schema)
|
|
225
256
|
}
|
|
@@ -234,7 +265,7 @@ module BetterAuth
|
|
|
234
265
|
end
|
|
235
266
|
end
|
|
236
267
|
|
|
237
|
-
def
|
|
268
|
+
def oauth2_link_account_endpoint(config)
|
|
238
269
|
Endpoint.new(
|
|
239
270
|
path: "/oauth2/link",
|
|
240
271
|
method: "POST",
|
|
@@ -242,6 +273,7 @@ module BetterAuth
|
|
|
242
273
|
openapi: {
|
|
243
274
|
operationId: "linkOAuth2",
|
|
244
275
|
description: "Link an OAuth2 account to the current user session",
|
|
276
|
+
requestBody: OpenAPI.json_request_body(generic_oauth_start_body_schema),
|
|
245
277
|
responses: {
|
|
246
278
|
"200" => OpenAPI.json_response("Authorization URL generated successfully for linking an OAuth2 account", generic_oauth_url_response_schema)
|
|
247
279
|
}
|
|
@@ -264,7 +296,7 @@ module BetterAuth
|
|
|
264
296
|
end
|
|
265
297
|
end
|
|
266
298
|
|
|
267
|
-
def
|
|
299
|
+
def oauth2_callback_endpoint(config)
|
|
268
300
|
Endpoint.new(
|
|
269
301
|
path: "/oauth2/callback/:providerId",
|
|
270
302
|
method: "GET",
|
|
@@ -289,9 +321,9 @@ module BetterAuth
|
|
|
289
321
|
provider = generic_oauth_provider!(config, provider_id)
|
|
290
322
|
state_data = generic_oauth_parse_state(ctx, query[:state].to_s)
|
|
291
323
|
error_url = state_data["errorURL"] || state_data["errorCallbackURL"] || "#{ctx.context.base_url}/error"
|
|
292
|
-
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)) }
|
|
293
325
|
|
|
294
|
-
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?
|
|
295
327
|
generic_oauth_validate_issuer!(ctx, provider, query, redirect_error)
|
|
296
328
|
|
|
297
329
|
tokens = begin
|
|
@@ -309,6 +341,7 @@ module BetterAuth
|
|
|
309
341
|
account_id = fetch_value(mapped_user, "id").to_s
|
|
310
342
|
redirect_error.call("email_is_missing") if email.empty?
|
|
311
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)
|
|
312
345
|
|
|
313
346
|
link = state_data["link"]
|
|
314
347
|
callback_url = state_data["callbackURL"] || "/"
|
|
@@ -321,21 +354,20 @@ module BetterAuth
|
|
|
321
354
|
if !existing && (provider[:disable_sign_up] || (provider[:disable_implicit_sign_up] && !state_data["requestSignUp"]))
|
|
322
355
|
redirect_error.call("signup_disabled")
|
|
323
356
|
end
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
"
|
|
329
|
-
|
|
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]
|
|
330
364
|
)
|
|
331
|
-
|
|
365
|
+
rescue APIError => error
|
|
366
|
+
raise if error.code.to_s.empty?
|
|
332
367
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
mapped_user.merge("email" => email, "name" => name, "id" => account_id),
|
|
337
|
-
generic_oauth_account_info(ctx, provider_id, account_id, tokens)
|
|
338
|
-
)
|
|
368
|
+
redirect_error.call(error.code, error.message)
|
|
369
|
+
end
|
|
370
|
+
redirect_error.call(session_data[:error].tr(" ", "_")) if session_data[:error]
|
|
339
371
|
generic_oauth_set_account_cookie(ctx, provider_id, account_id, session_data[:user]["id"])
|
|
340
372
|
Cookies.set_session_cookie(ctx, session_data)
|
|
341
373
|
raise ctx.redirect(existing ? callback_url : (state_data["newUserURL"] || state_data["newUserCallbackURL"] || callback_url))
|
|
@@ -352,13 +384,35 @@ module BetterAuth
|
|
|
352
384
|
)
|
|
353
385
|
end
|
|
354
386
|
|
|
355
|
-
def
|
|
387
|
+
def generic_oauth_start_body_schema
|
|
388
|
+
OpenAPI.object_schema(
|
|
389
|
+
{
|
|
390
|
+
providerId: {type: "string", description: "OAuth provider ID"},
|
|
391
|
+
callbackURL: {type: "string", description: "URL to redirect to after success"},
|
|
392
|
+
errorCallbackURL: {type: "string", description: "URL to redirect to after error"},
|
|
393
|
+
newUserCallbackURL: {type: "string", description: "URL to redirect to for new users"},
|
|
394
|
+
requestSignUp: {type: "boolean", description: "Whether this request is a sign-up flow"},
|
|
395
|
+
scopes: {type: "array", items: {type: "string"}, description: "Additional OAuth scopes"},
|
|
396
|
+
disableRedirect: {type: "boolean", description: "Return the URL instead of redirecting"},
|
|
397
|
+
additionalData: {type: "object", additionalProperties: true}
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def generic_oauth_authorization_url(ctx, provider, body, link:, code_verifier: nil, preserve_additional_data: false)
|
|
356
403
|
authorization_url = provider[:authorization_url] || generic_oauth_discovery(provider)["authorization_endpoint"]
|
|
357
404
|
token_url = provider[:token_url] || generic_oauth_discovery(provider)["token_endpoint"]
|
|
358
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?
|
|
359
406
|
|
|
360
|
-
code_verifier
|
|
361
|
-
|
|
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(
|
|
362
416
|
"callbackURL" => body[:callback_url] || body[:callbackURL] || "/",
|
|
363
417
|
"errorURL" => body[:error_callback_url] || body[:errorCallbackURL],
|
|
364
418
|
"newUserURL" => body[:new_user_callback_url] || body[:newUserCallbackURL],
|
|
@@ -368,79 +422,59 @@ module BetterAuth
|
|
|
368
422
|
"expiresAt" => Time.now.to_i + 600
|
|
369
423
|
)
|
|
370
424
|
state = generic_oauth_generate_state(ctx, state_data)
|
|
371
|
-
legacy_state = Crypto.sign_jwt(
|
|
372
|
-
{
|
|
373
|
-
"callbackURL" => body[:callback_url] || body[:callbackURL] || "/",
|
|
374
|
-
"errorURL" => body[:error_callback_url] || body[:errorCallbackURL],
|
|
375
|
-
"newUserURL" => body[:new_user_callback_url] || body[:newUserCallbackURL],
|
|
376
|
-
"requestSignUp" => body[:request_sign_up] || body[:requestSignUp],
|
|
377
|
-
"codeVerifier" => code_verifier,
|
|
378
|
-
"link" => link
|
|
379
|
-
},
|
|
380
|
-
ctx.context.secret,
|
|
381
|
-
expires_in: 600
|
|
382
|
-
)
|
|
383
|
-
state ||= legacy_state
|
|
384
425
|
|
|
385
426
|
uri = URI.parse(authorization_url.to_s)
|
|
386
427
|
params = URI.decode_www_form(uri.query.to_s)
|
|
387
|
-
params.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
["state", state]
|
|
392
|
-
])
|
|
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)
|
|
393
432
|
scopes = Array(body[:scopes]) + Array(provider[:scopes])
|
|
394
|
-
params
|
|
433
|
+
generic_oauth_set_query_param(params, "scope", scopes.join(" ")) unless scopes.empty?
|
|
395
434
|
if provider[:pkce]
|
|
396
|
-
params
|
|
397
|
-
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")
|
|
398
437
|
end
|
|
399
|
-
params
|
|
400
|
-
params
|
|
401
|
-
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]
|
|
402
441
|
authorization_params = if provider[:authorization_url_params].respond_to?(:call)
|
|
403
442
|
provider[:authorization_url_params].call(ctx)
|
|
404
443
|
else
|
|
405
444
|
provider[:authorization_url_params]
|
|
406
445
|
end
|
|
407
|
-
normalize_hash(authorization_params || {}).each { |key, value| params
|
|
446
|
+
normalize_hash(authorization_params || {}).each { |key, value| generic_oauth_set_query_param(params, key, value) }
|
|
408
447
|
uri.query = URI.encode_www_form(params)
|
|
409
448
|
uri.to_s
|
|
410
449
|
end
|
|
411
450
|
|
|
412
|
-
def
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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]
|
|
420
461
|
end
|
|
462
|
+
end
|
|
421
463
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
ctx.context.internal_adapter.create_verification_value(
|
|
425
|
-
identifier: state,
|
|
426
|
-
value: JSON.generate(state_data),
|
|
427
|
-
expiresAt: Time.now + 600
|
|
428
|
-
)
|
|
429
|
-
state
|
|
430
|
-
rescue
|
|
431
|
-
nil
|
|
464
|
+
def generic_oauth_generate_state(ctx, state_data)
|
|
465
|
+
OAuthState.generate(ctx, state_data)
|
|
432
466
|
end
|
|
433
467
|
|
|
434
468
|
def generic_oauth_exchange_token(ctx, provider, code, state_data)
|
|
435
469
|
token_callback = provider[:get_token]
|
|
436
470
|
if token_callback.respond_to?(:call)
|
|
437
|
-
return
|
|
471
|
+
return generic_oauth_normalize_tokens(token_callback.call(
|
|
438
472
|
code: code,
|
|
439
473
|
redirectURI: generic_oauth_redirect_uri(ctx, provider),
|
|
440
474
|
redirect_uri: generic_oauth_redirect_uri(ctx, provider),
|
|
441
475
|
codeVerifier: provider[:pkce] ? state_data["codeVerifier"] : nil,
|
|
442
476
|
code_verifier: provider[:pkce] ? state_data["codeVerifier"] : nil
|
|
443
|
-
))
|
|
477
|
+
), access_token_expires_in: provider[:access_token_expires_in])
|
|
444
478
|
end
|
|
445
479
|
|
|
446
480
|
token_url = provider[:token_url] || generic_oauth_discovery(provider)["token_endpoint"]
|
|
@@ -450,70 +484,37 @@ module BetterAuth
|
|
|
450
484
|
end
|
|
451
485
|
|
|
452
486
|
def generic_oauth_parse_state(ctx, state)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
cookie = ctx.context.create_auth_cookie("oauth_state")
|
|
459
|
-
encrypted = ctx.get_cookie(cookie.name)
|
|
460
|
-
unless encrypted
|
|
461
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "state_mismatch"))
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
begin
|
|
465
|
-
decrypted = Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: encrypted)
|
|
466
|
-
unless decrypted
|
|
467
|
-
Cookies.expire_cookie(ctx, cookie)
|
|
468
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "please_restart_the_process"))
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
parsed = JSON.parse(decrypted)
|
|
472
|
-
rescue JSON::ParserError
|
|
473
|
-
Cookies.expire_cookie(ctx, cookie)
|
|
474
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "please_restart_the_process"))
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
Cookies.expire_cookie(ctx, cookie)
|
|
478
|
-
if parsed["state"] != state
|
|
479
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "state_mismatch"))
|
|
480
|
-
end
|
|
481
|
-
if parsed["expiresAt"].to_i.positive? && parsed["expiresAt"].to_i < Time.now.to_i
|
|
482
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "state_mismatch"))
|
|
483
|
-
end
|
|
484
|
-
return parsed
|
|
485
|
-
else
|
|
486
|
-
verification = ctx.context.internal_adapter.find_verification_value(state)
|
|
487
|
-
if verification
|
|
488
|
-
cookie = ctx.context.create_auth_cookie("state")
|
|
489
|
-
cookie_state = ctx.get_signed_cookie(cookie.name, ctx.context.secret)
|
|
490
|
-
if cookie_state && cookie_state != state
|
|
491
|
-
Cookies.expire_cookie(ctx, cookie)
|
|
492
|
-
raise ctx.redirect(generic_oauth_error_url(generic_oauth_state_error_url(ctx), "state_mismatch"))
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
parsed = JSON.parse(verification.fetch("value"))
|
|
496
|
-
ctx.context.internal_adapter.delete_verification_value(verification.fetch("id"))
|
|
497
|
-
Cookies.expire_cookie(ctx, cookie) if cookie_state
|
|
498
|
-
return parsed
|
|
499
|
-
end
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
Crypto.verify_jwt(state.to_s, ctx.context.secret) || {}
|
|
503
|
-
rescue JSON::ParserError
|
|
504
|
-
{}
|
|
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))
|
|
505
492
|
end
|
|
506
493
|
|
|
507
494
|
def generic_oauth_state_error_url(ctx)
|
|
508
495
|
ctx.context.options.on_api_error[:error_url] || "#{ctx.context.base_url}/error"
|
|
509
496
|
end
|
|
510
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
|
+
|
|
511
502
|
def generic_oauth_user_info(provider, tokens)
|
|
512
503
|
callback = provider[:get_user_info]
|
|
513
|
-
|
|
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
|
|
514
510
|
|
|
515
511
|
id_token = tokens[:id_token] || tokens[:idToken]
|
|
516
|
-
|
|
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
|
|
517
518
|
|
|
518
519
|
user_info_url = provider[:user_info_url] || generic_oauth_discovery(provider)["userinfo_endpoint"]
|
|
519
520
|
return nil if user_info_url.to_s.empty?
|
|
@@ -521,7 +522,7 @@ module BetterAuth
|
|
|
521
522
|
uri = URI(user_info_url)
|
|
522
523
|
request = Net::HTTP::Get.new(uri)
|
|
523
524
|
request["authorization"] = "Bearer #{fetch_value(tokens, "accessToken")}"
|
|
524
|
-
response =
|
|
525
|
+
response = HTTPClient.request(uri, request)
|
|
525
526
|
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
526
527
|
|
|
527
528
|
generic_oauth_normalize_user_info(JSON.parse(response.body))
|
|
@@ -593,7 +594,7 @@ module BetterAuth
|
|
|
593
594
|
end
|
|
594
595
|
|
|
595
596
|
def generic_oauth_redirect_uri(ctx, provider)
|
|
596
|
-
provider[:redirect_uri] || provider[:redirectURI] || "#{ctx.context.
|
|
597
|
+
provider[:redirect_uri] || provider[:redirectURI] || "#{ctx.context.canonical_base_url}/oauth2/callback/#{provider[:provider_id]}"
|
|
597
598
|
end
|
|
598
599
|
|
|
599
600
|
def generic_oauth_validate_issuer!(ctx, provider, query, redirect_error)
|
|
@@ -615,7 +616,7 @@ module BetterAuth
|
|
|
615
616
|
normalize_hash(provider[:discovery_headers] || provider[:discoveryHeaders]).each do |key, value|
|
|
616
617
|
request[key.to_s.tr("_", "-")] = value.to_s
|
|
617
618
|
end
|
|
618
|
-
response =
|
|
619
|
+
response = HTTPClient.request(uri, request)
|
|
619
620
|
provider[:_discovery] = response.is_a?(Net::HTTPSuccess) ? JSON.parse(response.body) : {}
|
|
620
621
|
rescue
|
|
621
622
|
{}
|
|
@@ -649,16 +650,16 @@ module BetterAuth
|
|
|
649
650
|
form_data[key] = value unless form_data.key?(key)
|
|
650
651
|
end
|
|
651
652
|
request.set_form_data(form_data)
|
|
652
|
-
response =
|
|
653
|
+
response = HTTPClient.request(uri, request)
|
|
653
654
|
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
654
655
|
|
|
655
|
-
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])
|
|
656
657
|
rescue
|
|
657
658
|
nil
|
|
658
659
|
end
|
|
659
660
|
|
|
660
661
|
def generic_oauth_user_from_id_token(id_token)
|
|
661
|
-
payload = JWT.decode(id_token, nil, false).first
|
|
662
|
+
payload = ::JWT.decode(id_token, nil, false).first
|
|
662
663
|
normalize_hash(
|
|
663
664
|
id: payload["sub"],
|
|
664
665
|
email: payload["email"],
|
|
@@ -670,15 +671,17 @@ module BetterAuth
|
|
|
670
671
|
nil
|
|
671
672
|
end
|
|
672
673
|
|
|
673
|
-
def generic_oauth_normalize_tokens(data)
|
|
674
|
+
def generic_oauth_normalize_tokens(data, access_token_expires_in: nil)
|
|
674
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)
|
|
675
678
|
token_data.merge(
|
|
676
679
|
access_token: token_data[:access_token],
|
|
677
680
|
refresh_token: token_data[:refresh_token],
|
|
678
681
|
id_token: token_data[:id_token],
|
|
679
|
-
access_token_expires_at:
|
|
680
|
-
refresh_token_expires_at: generic_oauth_expiry_time(token_data[:refresh_token_expires_in]),
|
|
681
|
-
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]),
|
|
682
685
|
raw: token_data
|
|
683
686
|
).compact
|
|
684
687
|
end
|
|
@@ -714,7 +717,7 @@ module BetterAuth
|
|
|
714
717
|
uri = URI(url)
|
|
715
718
|
request = Net::HTTP::Get.new(uri)
|
|
716
719
|
normalize_hash(headers).each { |key, value| request[key.to_s.tr("_", "-")] = value.to_s }
|
|
717
|
-
response =
|
|
720
|
+
response = HTTPClient.request(uri, request)
|
|
718
721
|
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
719
722
|
|
|
720
723
|
JSON.parse(response.body)
|
|
@@ -740,7 +743,8 @@ module BetterAuth
|
|
|
740
743
|
pkce: data[:pkce],
|
|
741
744
|
disable_implicit_sign_up: data[:disable_implicit_sign_up],
|
|
742
745
|
disable_sign_up: data[:disable_sign_up],
|
|
743
|
-
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]
|
|
744
748
|
)
|
|
745
749
|
config[:scopes] = data[:scopes] if data[:scopes]
|
|
746
750
|
config.compact
|
|
@@ -753,7 +757,20 @@ module BetterAuth
|
|
|
753
757
|
id: provider_id,
|
|
754
758
|
name: provider_id,
|
|
755
759
|
get_user_info: ->(tokens) { generic_oauth_provider_user_info(provider, tokens) },
|
|
756
|
-
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
|
|
757
774
|
}
|
|
758
775
|
end
|
|
759
776
|
end
|
|
@@ -790,16 +807,21 @@ module BetterAuth
|
|
|
790
807
|
token_url_params = token_url_params.call(ctx) if token_url_params.respond_to?(:call)
|
|
791
808
|
normalize_hash(token_url_params || {}).each { |key, value| form_data[key] = value }
|
|
792
809
|
request.set_form_data(form_data.compact)
|
|
793
|
-
response =
|
|
810
|
+
response = HTTPClient.request(uri, request)
|
|
794
811
|
raise APIError.new("BAD_REQUEST", message: GENERIC_OAUTH_ERROR_CODES["INVALID_OAUTH_CONFIG"]) unless response.is_a?(Net::HTTPSuccess)
|
|
795
812
|
|
|
796
|
-
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])
|
|
797
814
|
end
|
|
798
815
|
|
|
799
|
-
def generic_oauth_error_url(base_url, error)
|
|
816
|
+
def generic_oauth_error_url(base_url, error, description = nil)
|
|
800
817
|
uri = URI.parse(base_url.to_s)
|
|
801
818
|
query = URI.decode_www_form(uri.query.to_s)
|
|
802
|
-
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
|
|
803
825
|
uri.query = URI.encode_www_form(query)
|
|
804
826
|
uri.to_s
|
|
805
827
|
end
|
|
@@ -83,7 +83,7 @@ module BetterAuth
|
|
|
83
83
|
request = Net::HTTP::Get.new(uri)
|
|
84
84
|
request["Add-Padding"] = "true"
|
|
85
85
|
request["User-Agent"] = "BetterAuth Password Checker"
|
|
86
|
-
response =
|
|
86
|
+
response = HTTPClient.request(uri, request)
|
|
87
87
|
unless response.is_a?(Net::HTTPSuccess)
|
|
88
88
|
raise APIError.new("INTERNAL_SERVER_ERROR", message: "Failed to check password. Status: #{response.code}")
|
|
89
89
|
end
|