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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +142 -95
  5. data/lib/better_auth/adapters/base.rb +64 -1
  6. data/lib/better_auth/adapters/internal_adapter.rb +197 -11
  7. data/lib/better_auth/adapters/join_support.rb +10 -0
  8. data/lib/better_auth/adapters/memory.rb +303 -54
  9. data/lib/better_auth/adapters/mssql.rb +7 -3
  10. data/lib/better_auth/adapters/sql.rb +390 -63
  11. data/lib/better_auth/api.rb +3 -24
  12. data/lib/better_auth/configuration.rb +48 -47
  13. data/lib/better_auth/context.rb +43 -67
  14. data/lib/better_auth/cookies.rb +119 -15
  15. data/lib/better_auth/core.rb +0 -2
  16. data/lib/better_auth/crypto.rb +4 -3
  17. data/lib/better_auth/doctor.rb +108 -0
  18. data/lib/better_auth/endpoint.rb +88 -5
  19. data/lib/better_auth/error.rb +3 -1
  20. data/lib/better_auth/http_client.rb +46 -0
  21. data/lib/better_auth/logger.rb +1 -1
  22. data/lib/better_auth/middleware/origin_check.rb +9 -1
  23. data/lib/better_auth/migration_plan.rb +15 -0
  24. data/lib/better_auth/oauth2.rb +1 -1
  25. data/lib/better_auth/oauth_state.rb +147 -0
  26. data/lib/better_auth/plugin_loader.rb +220 -0
  27. data/lib/better_auth/plugins/access.rb +10 -13
  28. data/lib/better_auth/plugins/admin.rb +85 -29
  29. data/lib/better_auth/plugins/anonymous.rb +2 -0
  30. data/lib/better_auth/plugins/api_key.rb +4 -7
  31. data/lib/better_auth/plugins/bearer.rb +8 -22
  32. data/lib/better_auth/plugins/captcha.rb +24 -6
  33. data/lib/better_auth/plugins/device_authorization.rb +77 -5
  34. data/lib/better_auth/plugins/dub.rb +10 -2
  35. data/lib/better_auth/plugins/email_otp.rb +30 -47
  36. data/lib/better_auth/plugins/generic_oauth.rb +162 -140
  37. data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
  38. data/lib/better_auth/plugins/i18n.rb +215 -0
  39. data/lib/better_auth/plugins/jwt.rb +44 -19
  40. data/lib/better_auth/plugins/magic_link.rb +18 -21
  41. data/lib/better_auth/plugins/oauth_popup.rb +330 -0
  42. data/lib/better_auth/plugins/oauth_protocol.rb +233 -46
  43. data/lib/better_auth/plugins/oauth_provider.rb +4 -7
  44. data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
  45. data/lib/better_auth/plugins/one_tap.rb +78 -65
  46. data/lib/better_auth/plugins/one_time_token.rb +1 -4
  47. data/lib/better_auth/plugins/open_api.rb +77 -9
  48. data/lib/better_auth/plugins/organization.rb +546 -102
  49. data/lib/better_auth/plugins/passkey.rb +4 -7
  50. data/lib/better_auth/plugins/phone_number.rb +25 -27
  51. data/lib/better_auth/plugins/scim.rb +4 -7
  52. data/lib/better_auth/plugins/siwe.rb +126 -16
  53. data/lib/better_auth/plugins/sso.rb +4 -7
  54. data/lib/better_auth/plugins/stripe.rb +4 -7
  55. data/lib/better_auth/plugins/two_factor.rb +205 -19
  56. data/lib/better_auth/plugins/username.rb +56 -13
  57. data/lib/better_auth/plugins.rb +57 -0
  58. data/lib/better_auth/rate_limiter.rb +300 -133
  59. data/lib/better_auth/request_ip.rb +123 -13
  60. data/lib/better_auth/router.rb +3 -3
  61. data/lib/better_auth/routes/account.rb +44 -8
  62. data/lib/better_auth/routes/email_verification.rb +93 -6
  63. data/lib/better_auth/routes/password.rb +6 -6
  64. data/lib/better_auth/routes/session.rb +7 -2
  65. data/lib/better_auth/routes/sign_in.rb +10 -1
  66. data/lib/better_auth/routes/sign_up.rb +45 -22
  67. data/lib/better_auth/routes/social.rb +197 -54
  68. data/lib/better_auth/routes/user.rb +17 -10
  69. data/lib/better_auth/schema/sql.rb +189 -34
  70. data/lib/better_auth/schema.rb +159 -5
  71. data/lib/better_auth/session.rb +29 -10
  72. data/lib/better_auth/session_store.rb +1 -1
  73. data/lib/better_auth/social_providers/apple.rb +6 -1
  74. data/lib/better_auth/social_providers/atlassian.rb +1 -0
  75. data/lib/better_auth/social_providers/base.rb +45 -23
  76. data/lib/better_auth/social_providers/cognito.rb +75 -5
  77. data/lib/better_auth/social_providers/facebook.rb +95 -1
  78. data/lib/better_auth/social_providers/figma.rb +2 -0
  79. data/lib/better_auth/social_providers/github.rb +2 -0
  80. data/lib/better_auth/social_providers/google.rb +12 -1
  81. data/lib/better_auth/social_providers/line.rb +35 -1
  82. data/lib/better_auth/social_providers/linear.rb +1 -0
  83. data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
  84. data/lib/better_auth/social_providers/naver.rb +24 -1
  85. data/lib/better_auth/social_providers/notion.rb +1 -0
  86. data/lib/better_auth/social_providers/paybin.rb +25 -1
  87. data/lib/better_auth/social_providers/paypal.rb +118 -2
  88. data/lib/better_auth/social_providers/reddit.rb +25 -4
  89. data/lib/better_auth/social_providers/salesforce.rb +1 -0
  90. data/lib/better_auth/social_providers/twitch.rb +24 -1
  91. data/lib/better_auth/social_providers/twitter.rb +36 -1
  92. data/lib/better_auth/social_providers/vk.rb +34 -1
  93. data/lib/better_auth/social_providers/wechat.rb +6 -4
  94. data/lib/better_auth/sql_migration.rb +566 -0
  95. data/lib/better_auth/version.rb +1 -1
  96. data/lib/better_auth.rb +14 -41
  97. metadata +14 -18
  98. data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
  99. data/lib/better_auth/plugins/mcp/config.rb +0 -51
  100. data/lib/better_auth/plugins/mcp/consent.rb +0 -31
  101. data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
  102. data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
  103. data/lib/better_auth/plugins/mcp/registration.rb +0 -31
  104. data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
  105. data/lib/better_auth/plugins/mcp/schema.rb +0 -91
  106. data/lib/better_auth/plugins/mcp/token.rb +0 -108
  107. data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
  108. data/lib/better_auth/plugins/mcp.rb +0 -215
  109. 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: primary_client_id(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
- post_form_json(option(opts, :token_endpoint, :tokenEndpoint) || token_endpoint, {
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: primary_client_id(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] || lambda do |token, _nonce = nil|
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(post_form_json(token_endpoint, {
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
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", open_timeout: 5, read_timeout: 5) do |http|
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
- domain = (options[:domain] || options[:issuer] || "https://cognito-idp.#{options[:region] || "us-east-1"}.amazonaws.com").to_s.sub(%r{/+\z}, "")
9
- Base.oauth_provider(
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: "#{domain}/oauth2/authorize",
15
- token_endpoint: "#{domain}/oauth2/token",
16
- user_info_endpoint: "#{domain}/oauth2/userinfo",
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
- provider[:verify_id_token] = provider[:options][:verify_id_token] || ->(token, _nonce = nil) { provider[:options][:disable_id_token_sign_in] ? false : !token.to_s.empty? }
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
@@ -15,6 +15,8 @@ module BetterAuth
15
15
  user_info_endpoint: "https://api.figma.com/v1/me",
16
16
  scopes: scopes,
17
17
  pkce: true,
18
+ require_code_verifier: true,
19
+ token_authentication: :basic,
18
20
  profile_map: ->(profile) {
19
21
  {
20
22
  id: profile["id"],
@@ -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] = provider[:options][:verify_id_token] || ->(token, _nonce = nil) { provider[:options][:disable_id_token_sign_in] ? false : !Base.decode_jwt_payload(token).empty? }
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.oauth_provider(
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