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
|
@@ -7,6 +7,7 @@ require "net/http"
|
|
|
7
7
|
require "openssl"
|
|
8
8
|
require "time"
|
|
9
9
|
require "uri"
|
|
10
|
+
require_relative "../http_client"
|
|
10
11
|
|
|
11
12
|
module BetterAuth
|
|
12
13
|
module SocialProviders
|
|
@@ -25,8 +26,9 @@ module BetterAuth
|
|
|
25
26
|
uri.to_s
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
def oauth_provider(id:, name:, client_id:, authorization_endpoint:, token_endpoint:, profile_map:, client_secret: nil, user_info_endpoint: nil, scopes: [], scope_separator: " ", pkce: false, auth_params: {}, token_params: {}, user_info_method: :get, user_info_headers: {}, user_info_body: nil, **options)
|
|
29
|
+
def oauth_provider(id:, name:, client_id:, authorization_endpoint:, token_endpoint:, profile_map:, client_secret: nil, user_info_endpoint: nil, scopes: [], scope_separator: " ", pkce: false, require_code_verifier: false, auth_params: {}, token_params: {}, token_authentication: :post, user_info_method: :get, user_info_headers: {}, user_info_body: nil, **options)
|
|
29
30
|
opts = normalize_options(options.merge(client_id: client_id, client_secret: client_secret))
|
|
31
|
+
primary_id = primary_client_id(client_id)
|
|
30
32
|
{
|
|
31
33
|
id: id,
|
|
32
34
|
name: name,
|
|
@@ -35,9 +37,11 @@ module BetterAuth
|
|
|
35
37
|
options: opts,
|
|
36
38
|
create_authorization_url: lambda do |data|
|
|
37
39
|
verifier = value(data, :code_verifier, :codeVerifier)
|
|
40
|
+
raise Error, "codeVerifier is required for #{name}" if require_code_verifier && verifier.to_s.empty?
|
|
41
|
+
|
|
38
42
|
selected_scopes = selected_scopes(scopes, opts, data)
|
|
39
43
|
params = {
|
|
40
|
-
client_id:
|
|
44
|
+
client_id: primary_id,
|
|
41
45
|
redirect_uri: opts[:redirect_uri] || value(data, :redirect_uri, :redirectURI),
|
|
42
46
|
response_type: "code",
|
|
43
47
|
scope: selected_scopes.empty? ? nil : selected_scopes.join(scope_separator),
|
|
@@ -50,28 +54,30 @@ module BetterAuth
|
|
|
50
54
|
authorization_url(option(opts, :authorization_endpoint, :authorizationEndpoint) || authorization_endpoint, params)
|
|
51
55
|
end,
|
|
52
56
|
validate_authorization_code: lambda do |data|
|
|
53
|
-
|
|
54
|
-
client_id: primary_client_id(client_id),
|
|
55
|
-
client_secret: client_secret,
|
|
57
|
+
token_form = {
|
|
56
58
|
code: value(data, :code),
|
|
57
59
|
code_verifier: value(data, :code_verifier, :codeVerifier),
|
|
58
60
|
grant_type: "authorization_code",
|
|
59
61
|
redirect_uri: opts[:redirect_uri] || value(data, :redirect_uri, :redirectURI)
|
|
60
|
-
}.merge(resolve_hash(token_params, data, opts))
|
|
62
|
+
}.merge(resolve_hash(token_params, data, opts))
|
|
63
|
+
post_token_form(
|
|
64
|
+
option(opts, :token_endpoint, :tokenEndpoint) || token_endpoint,
|
|
65
|
+
token_form,
|
|
66
|
+
client_id: primary_id,
|
|
67
|
+
client_secret: client_secret,
|
|
68
|
+
authentication: token_authentication
|
|
69
|
+
)
|
|
61
70
|
end,
|
|
62
71
|
refresh_access_token: opts[:refresh_access_token] || lambda do |refresh_token|
|
|
63
72
|
refresh_access_token(
|
|
64
73
|
option(opts, :token_endpoint, :tokenEndpoint) || token_endpoint,
|
|
65
74
|
refresh_token,
|
|
66
|
-
client_id:
|
|
67
|
-
client_secret: client_secret
|
|
75
|
+
client_id: primary_id,
|
|
76
|
+
client_secret: client_secret,
|
|
77
|
+
authentication: token_authentication
|
|
68
78
|
)
|
|
69
79
|
end,
|
|
70
|
-
verify_id_token: opts[:verify_id_token]
|
|
71
|
-
return false if opts[:disable_id_token_sign_in]
|
|
72
|
-
|
|
73
|
-
!decode_jwt_payload(token).empty?
|
|
74
|
-
end,
|
|
80
|
+
verify_id_token: opts[:verify_id_token],
|
|
75
81
|
get_user_info: lambda do |tokens|
|
|
76
82
|
custom = opts[:get_user_info]
|
|
77
83
|
profile = if custom
|
|
@@ -141,13 +147,27 @@ module BetterAuth
|
|
|
141
147
|
(method == :post) ? post_json(endpoint, resolved_body, resolved_headers) : get_json(endpoint, resolved_headers)
|
|
142
148
|
end
|
|
143
149
|
|
|
144
|
-
def refresh_access_token(token_endpoint, refresh_token, client_id:, client_secret: nil, extra_params: {})
|
|
145
|
-
normalize_tokens(
|
|
146
|
-
client_id: client_id,
|
|
147
|
-
client_secret: client_secret,
|
|
150
|
+
def refresh_access_token(token_endpoint, refresh_token, client_id:, client_secret: nil, extra_params: {}, authentication: :post)
|
|
151
|
+
normalize_tokens(post_token_form(token_endpoint, {
|
|
148
152
|
grant_type: "refresh_token",
|
|
149
153
|
refresh_token: refresh_token
|
|
150
|
-
}.merge(extra_params || {})))
|
|
154
|
+
}.merge(extra_params || {}), client_id: client_id, client_secret: client_secret, authentication: authentication))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def post_token_form(token_endpoint, form, client_id:, client_secret: nil, authentication: :post, headers: {})
|
|
158
|
+
case authentication&.to_sym
|
|
159
|
+
when :basic
|
|
160
|
+
post_form_json(token_endpoint, form.compact, basic_auth_headers(client_id, client_secret).merge(headers))
|
|
161
|
+
else
|
|
162
|
+
post_form_json(token_endpoint, {
|
|
163
|
+
client_id: client_id,
|
|
164
|
+
client_secret: client_secret
|
|
165
|
+
}.merge(form), headers)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def basic_auth_headers(client_id, client_secret)
|
|
170
|
+
{"Authorization" => "Basic #{Base64.strict_encode64("#{client_id}:#{client_secret}")}"}
|
|
151
171
|
end
|
|
152
172
|
|
|
153
173
|
def normalize_tokens(tokens, now: Time.now)
|
|
@@ -214,7 +234,11 @@ module BetterAuth
|
|
|
214
234
|
appBundleIdentifier: :app_bundle_identifier,
|
|
215
235
|
profilePhotoSize: :profile_photo_size,
|
|
216
236
|
disableProfilePhoto: :disable_profile_photo,
|
|
217
|
-
tenantId: :tenant_id
|
|
237
|
+
tenantId: :tenant_id,
|
|
238
|
+
userPoolId: :user_pool_id,
|
|
239
|
+
requireClientSecret: :require_client_secret,
|
|
240
|
+
configId: :config_id,
|
|
241
|
+
loginUrl: :login_url
|
|
218
242
|
}.each do |camel, snake|
|
|
219
243
|
normalized[snake] = normalized[camel] if normalized.key?(camel) && !normalized.key?(snake)
|
|
220
244
|
end
|
|
@@ -277,7 +301,7 @@ module BetterAuth
|
|
|
277
301
|
return nil if max_age && payload["iat"] && payload["iat"].to_i < Time.now.to_i - max_age.to_i
|
|
278
302
|
|
|
279
303
|
payload
|
|
280
|
-
rescue JWT::DecodeError, JSON::ParserError, ArgumentError, OpenSSL::PKey::PKeyError
|
|
304
|
+
rescue JWT::DecodeError, JSON::ParserError, ArgumentError, OpenSSL::PKey::PKeyError, Net::OpenTimeout, Net::ReadTimeout, SocketError, SystemCallError
|
|
281
305
|
nil
|
|
282
306
|
end
|
|
283
307
|
|
|
@@ -312,9 +336,7 @@ module BetterAuth
|
|
|
312
336
|
end
|
|
313
337
|
|
|
314
338
|
def request_json(uri, request)
|
|
315
|
-
|
|
316
|
-
http.request(request)
|
|
317
|
-
end
|
|
339
|
+
HTTPClient.request(uri, request)
|
|
318
340
|
end
|
|
319
341
|
|
|
320
342
|
def padded_base64(value)
|
|
@@ -5,15 +5,25 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def cognito(client_id:, client_secret: nil, scopes: ["openid", "profile", "email"], **options)
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
normalized = Base.normalize_options(options)
|
|
9
|
+
domain = normalized[:domain]
|
|
10
|
+
region = normalized[:region]
|
|
11
|
+
user_pool_id = normalized[:user_pool_id]
|
|
12
|
+
raise Error, "DOMAIN_AND_REGION_REQUIRED" if domain.to_s.empty? || region.to_s.empty? || user_pool_id.to_s.empty?
|
|
13
|
+
|
|
14
|
+
if normalized[:require_client_secret] && client_secret.to_s.empty?
|
|
15
|
+
raise Error, "CLIENT_SECRET_REQUIRED"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
clean_domain = domain.to_s.sub(%r{\Ahttps?://}, "").sub(%r{/+\z}, "")
|
|
19
|
+
provider = Base.oauth_provider(
|
|
10
20
|
id: "cognito",
|
|
11
21
|
name: "Cognito",
|
|
12
22
|
client_id: client_id,
|
|
13
23
|
client_secret: client_secret,
|
|
14
|
-
authorization_endpoint: "
|
|
15
|
-
token_endpoint: "
|
|
16
|
-
user_info_endpoint: "
|
|
24
|
+
authorization_endpoint: "https://#{clean_domain}/oauth2/authorize",
|
|
25
|
+
token_endpoint: "https://#{clean_domain}/oauth2/token",
|
|
26
|
+
user_info_endpoint: "https://#{clean_domain}/oauth2/userinfo",
|
|
17
27
|
scopes: scopes,
|
|
18
28
|
pkce: true,
|
|
19
29
|
profile_map: ->(profile) {
|
|
@@ -27,6 +37,66 @@ module BetterAuth
|
|
|
27
37
|
},
|
|
28
38
|
**options
|
|
29
39
|
)
|
|
40
|
+
create_authorization_url = provider.fetch(:create_authorization_url)
|
|
41
|
+
provider[:create_authorization_url] = lambda do |data|
|
|
42
|
+
create_authorization_url.call(data).sub(/scope=([^&]+)/) { "scope=#{$1.gsub("+", "%20")}" }
|
|
43
|
+
end
|
|
44
|
+
provider[:verify_id_token] ||= lambda do |token, nonce = nil|
|
|
45
|
+
return false if normalized[:disable_id_token_sign_in]
|
|
46
|
+
|
|
47
|
+
profile = Base.verify_jwt_with_jwks(
|
|
48
|
+
token,
|
|
49
|
+
jwks: normalized[:jwks],
|
|
50
|
+
jwks_endpoint: normalized[:jwks_endpoint] || "https://cognito-idp.#{region}.amazonaws.com/#{user_pool_id}/.well-known/jwks.json",
|
|
51
|
+
algorithms: ["RS256"],
|
|
52
|
+
issuers: "https://cognito-idp.#{region}.amazonaws.com/#{user_pool_id}",
|
|
53
|
+
audience: Array(client_id),
|
|
54
|
+
nonce: nonce,
|
|
55
|
+
max_age: 3600
|
|
56
|
+
)
|
|
57
|
+
!!profile&.fetch("sub", nil)
|
|
58
|
+
end
|
|
59
|
+
provider[:get_user_info] = lambda do |tokens|
|
|
60
|
+
custom = normalized[:get_user_info]
|
|
61
|
+
next custom.call(tokens) if custom
|
|
62
|
+
|
|
63
|
+
if Base.id_token(tokens)
|
|
64
|
+
profile = Base.decode_jwt_payload(Base.id_token(tokens))
|
|
65
|
+
unless profile.empty?
|
|
66
|
+
name = profile["name"] || profile["given_name"] || profile["username"] || ""
|
|
67
|
+
enriched = profile.merge("name" => name)
|
|
68
|
+
user = Base.apply_profile_mapping(
|
|
69
|
+
{
|
|
70
|
+
id: profile["sub"],
|
|
71
|
+
name: name,
|
|
72
|
+
email: profile["email"],
|
|
73
|
+
image: profile["picture"],
|
|
74
|
+
emailVerified: !!profile["email_verified"]
|
|
75
|
+
},
|
|
76
|
+
enriched,
|
|
77
|
+
normalized
|
|
78
|
+
)
|
|
79
|
+
next({user: user, data: enriched})
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
profile = Base.fetch_user_info("https://#{clean_domain}/oauth2/userinfo", tokens)
|
|
84
|
+
next nil unless profile
|
|
85
|
+
|
|
86
|
+
user = Base.apply_profile_mapping(
|
|
87
|
+
{
|
|
88
|
+
id: profile["sub"],
|
|
89
|
+
name: profile["name"] || profile["given_name"] || profile["username"] || "",
|
|
90
|
+
email: profile["email"],
|
|
91
|
+
image: profile["picture"],
|
|
92
|
+
emailVerified: !!profile["email_verified"]
|
|
93
|
+
},
|
|
94
|
+
profile,
|
|
95
|
+
normalized
|
|
96
|
+
)
|
|
97
|
+
{user: user, data: profile}
|
|
98
|
+
end
|
|
99
|
+
provider
|
|
30
100
|
end
|
|
31
101
|
end
|
|
32
102
|
end
|
|
@@ -5,6 +5,7 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def facebook(client_id:, client_secret:, scopes: ["email", "public_profile"], **options)
|
|
8
|
+
normalized = Base.normalize_options(options)
|
|
8
9
|
fields = Array(options[:fields] || %w[id name email picture email_verified]).join(",")
|
|
9
10
|
provider = Base.oauth_provider(
|
|
10
11
|
id: "facebook",
|
|
@@ -28,7 +29,100 @@ module BetterAuth
|
|
|
28
29
|
},
|
|
29
30
|
**options
|
|
30
31
|
)
|
|
31
|
-
|
|
32
|
+
verify_access_token = lambda do |access_token|
|
|
33
|
+
primary_client_id = Base.primary_client_id(client_id)
|
|
34
|
+
next nil if access_token.to_s.empty? || client_secret.to_s.empty?
|
|
35
|
+
|
|
36
|
+
response = Base.get_json(Base.authorization_url("https://graph.facebook.com/debug_token", {
|
|
37
|
+
input_token: access_token,
|
|
38
|
+
access_token: "#{primary_client_id}|#{client_secret}"
|
|
39
|
+
}))
|
|
40
|
+
data = response.is_a?(Hash) ? response["data"] : nil
|
|
41
|
+
next nil unless data.is_a?(Hash)
|
|
42
|
+
next nil unless data["is_valid"] == true
|
|
43
|
+
next nil unless Array(client_id).map(&:to_s).include?(data["app_id"].to_s)
|
|
44
|
+
next nil if data["app_id"].to_s.empty? || data["user_id"].to_s.empty?
|
|
45
|
+
|
|
46
|
+
data["user_id"]
|
|
47
|
+
rescue
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
provider[:verify_id_token] = lambda do |token, nonce = nil|
|
|
51
|
+
return false if normalized[:disable_id_token_sign_in]
|
|
52
|
+
|
|
53
|
+
custom = normalized[:verify_id_token]
|
|
54
|
+
next custom.call(token, nonce) if custom
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
unless token.to_s.split(".").length == 3
|
|
58
|
+
next !!verify_access_token.call(token)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
profile = Base.verify_jwt_with_jwks(
|
|
62
|
+
token,
|
|
63
|
+
jwks: normalized[:jwks],
|
|
64
|
+
jwks_endpoint: normalized[:jwks_endpoint] || "https://limited.facebook.com/.well-known/oauth/openid/jwks/",
|
|
65
|
+
algorithms: ["RS256"],
|
|
66
|
+
issuers: "https://www.facebook.com",
|
|
67
|
+
audience: Array(client_id),
|
|
68
|
+
nonce: nonce
|
|
69
|
+
)
|
|
70
|
+
!!profile
|
|
71
|
+
rescue
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
provider[:get_user_info] = lambda do |tokens|
|
|
76
|
+
custom = normalized[:get_user_info]
|
|
77
|
+
next custom.call(tokens) if custom
|
|
78
|
+
|
|
79
|
+
begin
|
|
80
|
+
token = Base.id_token(tokens)
|
|
81
|
+
if token && token.split(".").length == 3
|
|
82
|
+
profile = Base.decode_jwt_payload(token)
|
|
83
|
+
next nil if profile["sub"].to_s.empty?
|
|
84
|
+
|
|
85
|
+
user = Base.apply_profile_mapping(
|
|
86
|
+
{
|
|
87
|
+
id: profile["sub"],
|
|
88
|
+
name: profile["name"],
|
|
89
|
+
email: profile["email"],
|
|
90
|
+
image: profile["picture"],
|
|
91
|
+
emailVerified: false
|
|
92
|
+
},
|
|
93
|
+
profile.merge("email_verified" => false),
|
|
94
|
+
normalized
|
|
95
|
+
)
|
|
96
|
+
next({user: user, data: profile})
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
access_token = Base.access_token(tokens)
|
|
100
|
+
next nil if access_token.to_s.empty?
|
|
101
|
+
|
|
102
|
+
token_user_id = verify_access_token.call(access_token)
|
|
103
|
+
next nil unless token_user_id
|
|
104
|
+
|
|
105
|
+
profile = Base.fetch_user_info("https://graph.facebook.com/me?fields=#{URI.encode_www_form_component(fields)}", tokens)
|
|
106
|
+
next nil unless profile.is_a?(Hash)
|
|
107
|
+
next nil unless profile["id"] == token_user_id
|
|
108
|
+
|
|
109
|
+
picture = profile.dig("picture", "data", "url") || profile["picture"]
|
|
110
|
+
user = Base.apply_profile_mapping(
|
|
111
|
+
{
|
|
112
|
+
id: profile["id"] || profile["sub"],
|
|
113
|
+
name: profile["name"],
|
|
114
|
+
email: profile["email"],
|
|
115
|
+
image: picture,
|
|
116
|
+
emailVerified: !!profile["email_verified"]
|
|
117
|
+
},
|
|
118
|
+
profile,
|
|
119
|
+
normalized
|
|
120
|
+
)
|
|
121
|
+
{user: user, data: profile}
|
|
122
|
+
rescue
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
end
|
|
32
126
|
provider
|
|
33
127
|
end
|
|
34
128
|
end
|
|
@@ -43,6 +43,8 @@ module BetterAuth
|
|
|
43
43
|
"User-Agent" => "better-auth"
|
|
44
44
|
}
|
|
45
45
|
profile = Base.get_json(user_info_endpoint, headers)
|
|
46
|
+
next nil unless profile
|
|
47
|
+
|
|
46
48
|
emails = Base.get_json(emails_endpoint, headers)
|
|
47
49
|
primary = Array(emails).find { |email| email["email"] == profile["email"] } ||
|
|
48
50
|
Array(emails).find { |email| email["primary"] } ||
|
|
@@ -4,6 +4,14 @@ module BetterAuth
|
|
|
4
4
|
module SocialProviders
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
+
def google_hosted_domain_allowed?(configured_hosted_domain, token_hosted_domain)
|
|
8
|
+
return true if configured_hosted_domain.nil? || configured_hosted_domain.to_s.empty?
|
|
9
|
+
return false unless token_hosted_domain.is_a?(String) && !token_hosted_domain.strip.empty?
|
|
10
|
+
return true if configured_hosted_domain == "*"
|
|
11
|
+
|
|
12
|
+
token_hosted_domain == configured_hosted_domain
|
|
13
|
+
end
|
|
14
|
+
|
|
7
15
|
def google(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
|
|
8
16
|
normalized = Base.normalize_options(options)
|
|
9
17
|
primary_client_id = Base.primary_client_id(client_id)
|
|
@@ -12,6 +20,7 @@ module BetterAuth
|
|
|
12
20
|
name: "Google",
|
|
13
21
|
client_id: client_id,
|
|
14
22
|
client_secret: client_secret,
|
|
23
|
+
options: normalized,
|
|
15
24
|
create_authorization_url: lambda do |data|
|
|
16
25
|
verifier = data[:code_verifier] || data[:codeVerifier]
|
|
17
26
|
raise Error, "codeVerifier is required for Google" if verifier.to_s.empty?
|
|
@@ -57,7 +66,7 @@ module BetterAuth
|
|
|
57
66
|
audience: audiences,
|
|
58
67
|
nonce: nonce
|
|
59
68
|
)
|
|
60
|
-
!!profile&.fetch("sub", nil)
|
|
69
|
+
!!profile&.fetch("sub", nil) && google_hosted_domain_allowed?(normalized[:hd], profile["hd"])
|
|
61
70
|
end,
|
|
62
71
|
get_user_info: lambda do |tokens|
|
|
63
72
|
custom = normalized[:get_user_info]
|
|
@@ -65,6 +74,8 @@ module BetterAuth
|
|
|
65
74
|
next nil unless Base.id_token(tokens)
|
|
66
75
|
|
|
67
76
|
profile = Base.decode_jwt_payload(Base.id_token(tokens))
|
|
77
|
+
next nil unless google_hosted_domain_allowed?(normalized[:hd], profile["hd"])
|
|
78
|
+
|
|
68
79
|
user = Base.apply_profile_mapping(
|
|
69
80
|
{
|
|
70
81
|
id: profile["sub"],
|
|
@@ -5,6 +5,7 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def line(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options)
|
|
8
|
+
normalized = Base.normalize_options(options)
|
|
8
9
|
provider = Base.oauth_provider(
|
|
9
10
|
id: "line",
|
|
10
11
|
name: "LINE",
|
|
@@ -26,7 +27,40 @@ module BetterAuth
|
|
|
26
27
|
},
|
|
27
28
|
**options
|
|
28
29
|
)
|
|
29
|
-
provider[:verify_id_token]
|
|
30
|
+
provider[:verify_id_token] ||= lambda do |token, nonce = nil|
|
|
31
|
+
return false if normalized[:disable_id_token_sign_in]
|
|
32
|
+
|
|
33
|
+
profile = Base.post_form_json(
|
|
34
|
+
normalized[:verify_id_token_endpoint] || "https://api.line.me/oauth2/v2.1/verify",
|
|
35
|
+
{id_token: token, client_id: client_id, nonce: nonce}
|
|
36
|
+
)
|
|
37
|
+
next false unless profile
|
|
38
|
+
next false unless profile["aud"] == client_id
|
|
39
|
+
next false if profile["nonce"] && profile["nonce"] != nonce
|
|
40
|
+
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
provider[:get_user_info] = lambda do |tokens|
|
|
44
|
+
custom = normalized[:get_user_info]
|
|
45
|
+
next custom.call(tokens) if custom
|
|
46
|
+
|
|
47
|
+
profile = Base.id_token(tokens) ? Base.decode_jwt_payload(Base.id_token(tokens)) : {}
|
|
48
|
+
profile = Base.fetch_user_info("https://api.line.me/oauth2/v2.1/userinfo", tokens) if profile.empty?
|
|
49
|
+
next nil unless profile && !profile.empty?
|
|
50
|
+
|
|
51
|
+
user = Base.apply_profile_mapping(
|
|
52
|
+
{
|
|
53
|
+
id: profile["sub"] || profile["userId"],
|
|
54
|
+
name: profile["name"] || profile["displayName"] || "",
|
|
55
|
+
email: profile["email"],
|
|
56
|
+
image: profile["picture"] || profile["pictureUrl"],
|
|
57
|
+
emailVerified: false
|
|
58
|
+
},
|
|
59
|
+
profile,
|
|
60
|
+
normalized
|
|
61
|
+
)
|
|
62
|
+
{user: user, data: profile}
|
|
63
|
+
end
|
|
30
64
|
provider
|
|
31
65
|
end
|
|
32
66
|
end
|
|
@@ -33,6 +33,7 @@ module BetterAuth
|
|
|
33
33
|
"Authorization" => "Bearer #{Base.access_token(tokens)}"
|
|
34
34
|
)
|
|
35
35
|
return profile if Base.provider_user_info?(profile)
|
|
36
|
+
return nil unless profile
|
|
36
37
|
|
|
37
38
|
mapped = provider[:options][:map_profile_to_user]&.call(profile) || {}
|
|
38
39
|
viewer = profile.dig("data", "viewer") || {}
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module BetterAuth
|
|
4
4
|
module SocialProviders
|
|
5
|
+
MICROSOFT_CONSUMER_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad"
|
|
6
|
+
private_constant :MICROSOFT_CONSUMER_TENANT_ID
|
|
7
|
+
|
|
5
8
|
module_function
|
|
6
9
|
|
|
7
10
|
def microsoft_entra_id(client_id:, client_secret:, tenant_id: "common", scopes: ["openid", "profile", "email", "User.Read", "offline_access"], **options)
|
|
@@ -81,6 +84,12 @@ module BetterAuth
|
|
|
81
84
|
nonce: nonce
|
|
82
85
|
)
|
|
83
86
|
|
|
87
|
+
tid = profile&.fetch("tid", nil)
|
|
88
|
+
next false unless tid.is_a?(String)
|
|
89
|
+
next false unless profile["iss"] == "#{authority.to_s.sub(%r{/+\z}, "")}/#{tid}/v2.0"
|
|
90
|
+
next false if tenant_id.to_s == "organizations" && tid == MICROSOFT_CONSUMER_TENANT_ID
|
|
91
|
+
next false if tenant_id.to_s == "consumers" && tid != MICROSOFT_CONSUMER_TENANT_ID
|
|
92
|
+
|
|
84
93
|
!!(profile&.fetch("sub", nil) || profile&.fetch("oid", nil))
|
|
85
94
|
end,
|
|
86
95
|
get_user_info: lambda do |tokens|
|
|
@@ -5,7 +5,8 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def naver(client_id:, client_secret:, scopes: ["profile", "email"], **options)
|
|
8
|
-
Base.
|
|
8
|
+
normalized = Base.normalize_options(options)
|
|
9
|
+
provider = Base.oauth_provider(
|
|
9
10
|
id: "naver",
|
|
10
11
|
name: "Naver",
|
|
11
12
|
client_id: client_id,
|
|
@@ -26,6 +27,28 @@ module BetterAuth
|
|
|
26
27
|
},
|
|
27
28
|
**options
|
|
28
29
|
)
|
|
30
|
+
provider[:get_user_info] = lambda do |tokens|
|
|
31
|
+
custom = normalized[:get_user_info]
|
|
32
|
+
next custom.call(tokens) if custom
|
|
33
|
+
|
|
34
|
+
profile = Base.fetch_user_info("https://openapi.naver.com/v1/nid/me", tokens)
|
|
35
|
+
next nil unless profile && profile["resultcode"] == "00"
|
|
36
|
+
|
|
37
|
+
data = profile["response"] || {}
|
|
38
|
+
user = Base.apply_profile_mapping(
|
|
39
|
+
{
|
|
40
|
+
id: data["id"],
|
|
41
|
+
name: data["name"] || data["nickname"] || "",
|
|
42
|
+
email: data["email"],
|
|
43
|
+
image: data["profile_image"],
|
|
44
|
+
emailVerified: false
|
|
45
|
+
},
|
|
46
|
+
profile,
|
|
47
|
+
normalized
|
|
48
|
+
)
|
|
49
|
+
{user: user, data: profile}
|
|
50
|
+
end
|
|
51
|
+
provider
|
|
29
52
|
end
|
|
30
53
|
end
|
|
31
54
|
end
|
|
@@ -15,6 +15,7 @@ module BetterAuth
|
|
|
15
15
|
user_info_endpoint: "https://api.notion.com/v1/users/me",
|
|
16
16
|
scopes: scopes,
|
|
17
17
|
auth_params: {owner: "user"},
|
|
18
|
+
token_authentication: :basic,
|
|
18
19
|
user_info_headers: {"Notion-Version" => "2022-06-28"},
|
|
19
20
|
profile_map: ->(profile) {
|
|
20
21
|
user = profile.dig("bot", "owner", "user") || profile
|
|
@@ -5,8 +5,9 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def paybin(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
|
|
8
|
+
normalized = Base.normalize_options(options)
|
|
8
9
|
issuer = (options[:issuer] || "https://idp.paybin.io").to_s.sub(%r{/+\z}, "")
|
|
9
|
-
Base.oauth_provider(
|
|
10
|
+
provider = Base.oauth_provider(
|
|
10
11
|
id: "paybin",
|
|
11
12
|
name: "Paybin",
|
|
12
13
|
client_id: client_id,
|
|
@@ -15,6 +16,7 @@ module BetterAuth
|
|
|
15
16
|
token_endpoint: "#{issuer}/oauth2/token",
|
|
16
17
|
scopes: scopes,
|
|
17
18
|
pkce: true,
|
|
19
|
+
require_code_verifier: true,
|
|
18
20
|
profile_map: ->(profile) {
|
|
19
21
|
{
|
|
20
22
|
id: profile["sub"],
|
|
@@ -26,6 +28,28 @@ module BetterAuth
|
|
|
26
28
|
},
|
|
27
29
|
**options
|
|
28
30
|
)
|
|
31
|
+
provider[:get_user_info] = lambda do |tokens|
|
|
32
|
+
custom = normalized[:get_user_info]
|
|
33
|
+
next custom.call(tokens) if custom
|
|
34
|
+
next nil unless Base.id_token(tokens)
|
|
35
|
+
|
|
36
|
+
profile = Base.decode_jwt_payload(Base.id_token(tokens))
|
|
37
|
+
next nil if profile.empty?
|
|
38
|
+
|
|
39
|
+
user = Base.apply_profile_mapping(
|
|
40
|
+
{
|
|
41
|
+
id: profile["sub"],
|
|
42
|
+
name: profile["name"] || profile["preferred_username"] || "",
|
|
43
|
+
email: profile["email"],
|
|
44
|
+
image: profile["picture"],
|
|
45
|
+
emailVerified: !!profile["email_verified"]
|
|
46
|
+
},
|
|
47
|
+
profile,
|
|
48
|
+
normalized
|
|
49
|
+
)
|
|
50
|
+
{user: user, data: profile}
|
|
51
|
+
end
|
|
52
|
+
provider
|
|
29
53
|
end
|
|
30
54
|
end
|
|
31
55
|
end
|