better_auth 0.1.1 → 0.3.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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +110 -18
  4. data/lib/better_auth/adapters/base.rb +49 -0
  5. data/lib/better_auth/adapters/internal_adapter.rb +589 -0
  6. data/lib/better_auth/adapters/memory.rb +235 -0
  7. data/lib/better_auth/adapters/mongodb.rb +9 -0
  8. data/lib/better_auth/adapters/mssql.rb +42 -0
  9. data/lib/better_auth/adapters/mysql.rb +33 -0
  10. data/lib/better_auth/adapters/postgres.rb +17 -0
  11. data/lib/better_auth/adapters/sql.rb +441 -0
  12. data/lib/better_auth/adapters/sqlite.rb +20 -0
  13. data/lib/better_auth/api.rb +226 -0
  14. data/lib/better_auth/api_error.rb +53 -0
  15. data/lib/better_auth/auth.rb +42 -0
  16. data/lib/better_auth/configuration.rb +399 -0
  17. data/lib/better_auth/context.rb +211 -0
  18. data/lib/better_auth/cookies.rb +278 -0
  19. data/lib/better_auth/core.rb +37 -1
  20. data/lib/better_auth/crypto/jwe.rb +76 -0
  21. data/lib/better_auth/crypto.rb +191 -0
  22. data/lib/better_auth/database_hooks.rb +114 -0
  23. data/lib/better_auth/endpoint.rb +326 -0
  24. data/lib/better_auth/error.rb +52 -0
  25. data/lib/better_auth/middleware/origin_check.rb +128 -0
  26. data/lib/better_auth/password.rb +120 -0
  27. data/lib/better_auth/plugin.rb +142 -0
  28. data/lib/better_auth/plugin_context.rb +16 -0
  29. data/lib/better_auth/plugin_registry.rb +67 -0
  30. data/lib/better_auth/plugins/access.rb +87 -0
  31. data/lib/better_auth/plugins/additional_fields.rb +29 -0
  32. data/lib/better_auth/plugins/admin/schema.rb +28 -0
  33. data/lib/better_auth/plugins/admin.rb +518 -0
  34. data/lib/better_auth/plugins/anonymous.rb +198 -0
  35. data/lib/better_auth/plugins/api_key.rb +16 -0
  36. data/lib/better_auth/plugins/bearer.rb +128 -0
  37. data/lib/better_auth/plugins/captcha.rb +159 -0
  38. data/lib/better_auth/plugins/custom_session.rb +84 -0
  39. data/lib/better_auth/plugins/device_authorization.rb +302 -0
  40. data/lib/better_auth/plugins/email_otp.rb +536 -0
  41. data/lib/better_auth/plugins/expo.rb +88 -0
  42. data/lib/better_auth/plugins/generic_oauth.rb +780 -0
  43. data/lib/better_auth/plugins/have_i_been_pwned.rb +94 -0
  44. data/lib/better_auth/plugins/jwt.rb +482 -0
  45. data/lib/better_auth/plugins/last_login_method.rb +92 -0
  46. data/lib/better_auth/plugins/magic_link.rb +181 -0
  47. data/lib/better_auth/plugins/mcp.rb +342 -0
  48. data/lib/better_auth/plugins/multi_session.rb +173 -0
  49. data/lib/better_auth/plugins/oauth_protocol.rb +694 -0
  50. data/lib/better_auth/plugins/oauth_provider.rb +16 -0
  51. data/lib/better_auth/plugins/oauth_proxy.rb +257 -0
  52. data/lib/better_auth/plugins/oidc_provider.rb +597 -0
  53. data/lib/better_auth/plugins/one_tap.rb +154 -0
  54. data/lib/better_auth/plugins/one_time_token.rb +106 -0
  55. data/lib/better_auth/plugins/open_api.rb +489 -0
  56. data/lib/better_auth/plugins/organization/schema.rb +106 -0
  57. data/lib/better_auth/plugins/organization.rb +995 -0
  58. data/lib/better_auth/plugins/passkey.rb +16 -0
  59. data/lib/better_auth/plugins/phone_number.rb +321 -0
  60. data/lib/better_auth/plugins/scim.rb +16 -0
  61. data/lib/better_auth/plugins/siwe.rb +242 -0
  62. data/lib/better_auth/plugins/sso.rb +16 -0
  63. data/lib/better_auth/plugins/stripe.rb +16 -0
  64. data/lib/better_auth/plugins/two_factor.rb +514 -0
  65. data/lib/better_auth/plugins/username.rb +278 -0
  66. data/lib/better_auth/plugins.rb +46 -0
  67. data/lib/better_auth/rate_limiter.rb +232 -0
  68. data/lib/better_auth/request_ip.rb +70 -0
  69. data/lib/better_auth/router.rb +378 -0
  70. data/lib/better_auth/routes/account.rb +211 -0
  71. data/lib/better_auth/routes/email_verification.rb +111 -0
  72. data/lib/better_auth/routes/error.rb +102 -0
  73. data/lib/better_auth/routes/ok.rb +15 -0
  74. data/lib/better_auth/routes/password.rb +183 -0
  75. data/lib/better_auth/routes/session.rb +160 -0
  76. data/lib/better_auth/routes/sign_in.rb +90 -0
  77. data/lib/better_auth/routes/sign_out.rb +15 -0
  78. data/lib/better_auth/routes/sign_up.rb +196 -0
  79. data/lib/better_auth/routes/social.rb +367 -0
  80. data/lib/better_auth/routes/user.rb +205 -0
  81. data/lib/better_auth/schema/sql.rb +202 -0
  82. data/lib/better_auth/schema.rb +291 -0
  83. data/lib/better_auth/session.rb +122 -0
  84. data/lib/better_auth/session_store.rb +91 -0
  85. data/lib/better_auth/social_providers/apple.rb +91 -0
  86. data/lib/better_auth/social_providers/atlassian.rb +32 -0
  87. data/lib/better_auth/social_providers/base.rb +325 -0
  88. data/lib/better_auth/social_providers/cognito.rb +32 -0
  89. data/lib/better_auth/social_providers/discord.rb +81 -0
  90. data/lib/better_auth/social_providers/dropbox.rb +33 -0
  91. data/lib/better_auth/social_providers/facebook.rb +35 -0
  92. data/lib/better_auth/social_providers/figma.rb +31 -0
  93. data/lib/better_auth/social_providers/github.rb +74 -0
  94. data/lib/better_auth/social_providers/gitlab.rb +67 -0
  95. data/lib/better_auth/social_providers/google.rb +90 -0
  96. data/lib/better_auth/social_providers/huggingface.rb +31 -0
  97. data/lib/better_auth/social_providers/kakao.rb +32 -0
  98. data/lib/better_auth/social_providers/kick.rb +32 -0
  99. data/lib/better_auth/social_providers/line.rb +33 -0
  100. data/lib/better_auth/social_providers/linear.rb +44 -0
  101. data/lib/better_auth/social_providers/linkedin.rb +30 -0
  102. data/lib/better_auth/social_providers/microsoft_entra_id.rb +137 -0
  103. data/lib/better_auth/social_providers/naver.rb +31 -0
  104. data/lib/better_auth/social_providers/notion.rb +33 -0
  105. data/lib/better_auth/social_providers/paybin.rb +31 -0
  106. data/lib/better_auth/social_providers/paypal.rb +36 -0
  107. data/lib/better_auth/social_providers/polar.rb +31 -0
  108. data/lib/better_auth/social_providers/railway.rb +49 -0
  109. data/lib/better_auth/social_providers/reddit.rb +32 -0
  110. data/lib/better_auth/social_providers/roblox.rb +31 -0
  111. data/lib/better_auth/social_providers/salesforce.rb +38 -0
  112. data/lib/better_auth/social_providers/slack.rb +30 -0
  113. data/lib/better_auth/social_providers/spotify.rb +31 -0
  114. data/lib/better_auth/social_providers/tiktok.rb +35 -0
  115. data/lib/better_auth/social_providers/twitch.rb +39 -0
  116. data/lib/better_auth/social_providers/twitter.rb +32 -0
  117. data/lib/better_auth/social_providers/vercel.rb +47 -0
  118. data/lib/better_auth/social_providers/vk.rb +34 -0
  119. data/lib/better_auth/social_providers/wechat.rb +104 -0
  120. data/lib/better_auth/social_providers/zoom.rb +31 -0
  121. data/lib/better_auth/social_providers.rb +38 -0
  122. data/lib/better_auth/version.rb +1 -1
  123. data/lib/better_auth.rb +86 -2
  124. metadata +233 -21
  125. data/.ruby-version +0 -1
  126. data/.standard.yml +0 -12
  127. data/.vscode/settings.json +0 -22
  128. data/AGENTS.md +0 -50
  129. data/CLAUDE.md +0 -1
  130. data/CODE_OF_CONDUCT.md +0 -173
  131. data/CONTRIBUTING.md +0 -187
  132. data/Gemfile +0 -12
  133. data/Makefile +0 -207
  134. data/Rakefile +0 -25
  135. data/SECURITY.md +0 -28
  136. data/docker-compose.yml +0 -63
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def google(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
8
+ normalized = Base.normalize_options(options)
9
+ primary_client_id = Base.primary_client_id(client_id)
10
+ {
11
+ id: "google",
12
+ name: "Google",
13
+ client_id: client_id,
14
+ client_secret: client_secret,
15
+ create_authorization_url: lambda do |data|
16
+ verifier = data[:code_verifier] || data[:codeVerifier]
17
+ raise Error, "codeVerifier is required for Google" if verifier.to_s.empty?
18
+
19
+ Base.authorization_url(options[:authorization_endpoint] || "https://accounts.google.com/o/oauth2/v2/auth", {
20
+ client_id: primary_client_id,
21
+ redirect_uri: data[:redirect_uri] || data[:redirectURI],
22
+ response_type: "code",
23
+ scope: Base.selected_scopes(scopes, normalized, data),
24
+ state: data[:state],
25
+ code_challenge: verifier && Base.pkce_challenge(verifier),
26
+ code_challenge_method: verifier && "S256",
27
+ login_hint: data[:loginHint] || data[:login_hint],
28
+ prompt: options[:prompt],
29
+ access_type: options[:access_type] || options[:accessType] || "offline",
30
+ display: data[:display] || options[:display],
31
+ hd: options[:hd],
32
+ include_granted_scopes: "true"
33
+ })
34
+ end,
35
+ validate_authorization_code: lambda do |data|
36
+ Base.post_form("https://oauth2.googleapis.com/token", {
37
+ client_id: primary_client_id,
38
+ client_secret: client_secret,
39
+ code: data[:code],
40
+ code_verifier: data[:code_verifier] || data[:codeVerifier],
41
+ grant_type: "authorization_code",
42
+ redirect_uri: data[:redirect_uri] || data[:redirectURI]
43
+ })
44
+ end,
45
+ verify_id_token: normalized[:verify_id_token] || lambda do |token, nonce = nil|
46
+ return false if normalized[:disable_id_token_sign_in]
47
+
48
+ audiences = Array(client_id)
49
+ return false if audiences.empty?
50
+
51
+ profile = Base.verify_jwt_with_jwks(
52
+ token,
53
+ jwks: normalized[:jwks],
54
+ jwks_endpoint: normalized[:jwks_endpoint] || "https://www.googleapis.com/oauth2/v3/certs",
55
+ algorithms: ["RS256"],
56
+ issuers: ["https://accounts.google.com", "accounts.google.com"],
57
+ audience: audiences,
58
+ nonce: nonce
59
+ )
60
+ !!profile&.fetch("sub", nil)
61
+ end,
62
+ get_user_info: lambda do |tokens|
63
+ custom = normalized[:get_user_info]
64
+ next custom.call(tokens) if custom
65
+ next nil unless Base.id_token(tokens)
66
+
67
+ profile = Base.decode_jwt_payload(Base.id_token(tokens))
68
+ user = Base.apply_profile_mapping(
69
+ {
70
+ id: profile["sub"],
71
+ email: profile["email"],
72
+ name: profile["name"],
73
+ image: profile["picture"],
74
+ emailVerified: !!profile["email_verified"]
75
+ },
76
+ profile,
77
+ normalized
78
+ )
79
+ {
80
+ user: user,
81
+ data: profile
82
+ }
83
+ end,
84
+ refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
85
+ Base.refresh_access_token("https://oauth2.googleapis.com/token", refresh_token, client_id: primary_client_id, client_secret: client_secret)
86
+ end
87
+ }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def huggingface(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options)
8
+ Base.oauth_provider(
9
+ id: "huggingface",
10
+ name: "Hugging Face",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://huggingface.co/oauth/authorize",
14
+ token_endpoint: "https://huggingface.co/oauth/token",
15
+ user_info_endpoint: "https://huggingface.co/oauth/userinfo",
16
+ scopes: scopes,
17
+ pkce: true,
18
+ profile_map: ->(profile) {
19
+ {
20
+ id: profile["sub"],
21
+ name: profile["name"] || profile["preferred_username"] || "",
22
+ email: profile["email"],
23
+ image: profile["picture"],
24
+ emailVerified: !!profile["email_verified"]
25
+ }
26
+ },
27
+ **options
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def kakao(client_id:, client_secret:, scopes: ["account_email", "profile_image", "profile_nickname"], **options)
8
+ Base.oauth_provider(
9
+ id: "kakao",
10
+ name: "Kakao",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://kauth.kakao.com/oauth/authorize",
14
+ token_endpoint: "https://kauth.kakao.com/oauth/token",
15
+ user_info_endpoint: "https://kapi.kakao.com/v2/user/me",
16
+ scopes: scopes,
17
+ profile_map: ->(profile) {
18
+ account = profile["kakao_account"] || {}
19
+ kakao_profile = account["profile"] || {}
20
+ {
21
+ id: profile["id"].to_s,
22
+ name: kakao_profile["nickname"] || account["name"] || "",
23
+ email: account["email"],
24
+ image: kakao_profile["profile_image_url"] || kakao_profile["thumbnail_image_url"],
25
+ emailVerified: !!account["is_email_valid"] && !!account["is_email_verified"]
26
+ }
27
+ },
28
+ **options
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def kick(client_id:, client_secret:, scopes: ["user:read"], **options)
8
+ Base.oauth_provider(
9
+ id: "kick",
10
+ name: "Kick",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://id.kick.com/oauth/authorize",
14
+ token_endpoint: "https://id.kick.com/oauth/token",
15
+ user_info_endpoint: "https://api.kick.com/public/v1/users",
16
+ scopes: scopes,
17
+ pkce: true,
18
+ profile_map: ->(profile) {
19
+ user = Array(profile["data"]).first || profile
20
+ {
21
+ id: user["user_id"],
22
+ name: user["name"],
23
+ email: user["email"],
24
+ image: user["profile_picture"],
25
+ emailVerified: false
26
+ }
27
+ },
28
+ **options
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def line(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options)
8
+ provider = Base.oauth_provider(
9
+ id: "line",
10
+ name: "LINE",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://access.line.me/oauth2/v2.1/authorize",
14
+ token_endpoint: "https://api.line.me/oauth2/v2.1/token",
15
+ user_info_endpoint: "https://api.line.me/oauth2/v2.1/userinfo",
16
+ scopes: scopes,
17
+ pkce: true,
18
+ profile_map: ->(profile) {
19
+ {
20
+ id: profile["sub"] || profile["userId"],
21
+ name: profile["name"] || profile["displayName"] || "",
22
+ email: profile["email"],
23
+ image: profile["picture"] || profile["pictureUrl"],
24
+ emailVerified: false
25
+ }
26
+ },
27
+ **options
28
+ )
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
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def linear(client_id:, client_secret:, scopes: ["read"], **options)
8
+ provider = Base.oauth_provider(
9
+ id: "linear",
10
+ name: "Linear",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://linear.app/oauth/authorize",
14
+ token_endpoint: "https://api.linear.app/oauth/token",
15
+ scopes: scopes,
16
+ profile_map: ->(profile) {
17
+ viewer = profile.dig("data", "viewer") || {}
18
+ {
19
+ id: viewer["id"],
20
+ name: viewer["name"],
21
+ email: viewer["email"],
22
+ image: viewer["avatarUrl"],
23
+ emailVerified: false
24
+ }
25
+ },
26
+ **options
27
+ )
28
+ provider[:get_user_info] = lambda do |tokens|
29
+ custom = Base.option(provider[:options], :get_user_info)
30
+ profile = custom ? custom.call(tokens) : Base.post_json(
31
+ "https://api.linear.app/graphql",
32
+ {query: "{ viewer { id name email avatarUrl active createdAt updatedAt } }"},
33
+ "Authorization" => "Bearer #{Base.access_token(tokens)}"
34
+ )
35
+ return profile if Base.provider_user_info?(profile)
36
+
37
+ mapped = provider[:options][:map_profile_to_user]&.call(profile) || {}
38
+ viewer = profile.dig("data", "viewer") || {}
39
+ {user: {id: viewer["id"], name: viewer["name"], email: viewer["email"], image: viewer["avatarUrl"], emailVerified: false}.merge(mapped), data: profile}
40
+ end
41
+ provider
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def linkedin(client_id:, client_secret:, scopes: ["profile", "email", "openid"], **options)
8
+ Base.oauth_provider(
9
+ id: "linkedin",
10
+ name: "Linkedin",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://www.linkedin.com/oauth/v2/authorization",
14
+ token_endpoint: "https://www.linkedin.com/oauth/v2/accessToken",
15
+ user_info_endpoint: "https://api.linkedin.com/v2/userinfo",
16
+ scopes: scopes,
17
+ profile_map: ->(profile) {
18
+ {
19
+ id: profile["sub"],
20
+ name: profile["name"],
21
+ email: profile["email"],
22
+ image: profile["picture"],
23
+ emailVerified: !!profile["email_verified"]
24
+ }
25
+ },
26
+ **options
27
+ )
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def microsoft_entra_id(client_id:, client_secret:, tenant_id: "common", scopes: ["openid", "profile", "email", "User.Read", "offline_access"], **options)
8
+ normalized = Base.normalize_options(options)
9
+ microsoft_provider(
10
+ provider_id: "microsoft-entra-id",
11
+ provider_name: "Microsoft Entra ID",
12
+ client_id: client_id,
13
+ client_secret: client_secret,
14
+ tenant_id: normalized[:tenant_id] || tenant_id,
15
+ scopes: scopes,
16
+ **options
17
+ )
18
+ end
19
+
20
+ def microsoft(client_id:, client_secret: nil, tenant_id: "common", scopes: ["openid", "profile", "email", "User.Read", "offline_access"], **options)
21
+ normalized = Base.normalize_options(options)
22
+ microsoft_provider(
23
+ provider_id: "microsoft",
24
+ provider_name: "Microsoft EntraID",
25
+ client_id: client_id,
26
+ client_secret: client_secret,
27
+ tenant_id: normalized[:tenant_id] || tenant_id,
28
+ scopes: scopes,
29
+ **options
30
+ )
31
+ end
32
+
33
+ def microsoft_provider(provider_id:, provider_name:, client_id:, client_secret:, tenant_id:, scopes:, **options)
34
+ authority = options[:authority] || "https://login.microsoftonline.com"
35
+ base = "#{authority.to_s.sub(%r{/+\z}, "")}/#{tenant_id}/oauth2/v2.0"
36
+ normalized = Base.normalize_options(options)
37
+ primary_client_id = Base.primary_client_id(client_id)
38
+ {
39
+ id: provider_id,
40
+ name: provider_name,
41
+ client_id: client_id,
42
+ client_secret: client_secret,
43
+ create_authorization_url: lambda do |data|
44
+ verifier = data[:code_verifier] || data[:codeVerifier]
45
+ Base.authorization_url(options[:authorization_endpoint] || "#{base}/authorize", {
46
+ client_id: primary_client_id,
47
+ redirect_uri: data[:redirect_uri] || data[:redirectURI],
48
+ response_type: "code",
49
+ scope: Base.selected_scopes(scopes, normalized, data),
50
+ state: data[:state],
51
+ code_challenge: verifier && Base.pkce_challenge(verifier),
52
+ code_challenge_method: verifier && "S256",
53
+ login_hint: data[:loginHint] || data[:login_hint],
54
+ prompt: options[:prompt]
55
+ })
56
+ end,
57
+ validate_authorization_code: lambda do |data|
58
+ Base.post_form("#{base}/token", {
59
+ client_id: primary_client_id,
60
+ client_secret: client_secret,
61
+ code: data[:code],
62
+ code_verifier: data[:code_verifier] || data[:codeVerifier],
63
+ grant_type: "authorization_code",
64
+ redirect_uri: data[:redirect_uri] || data[:redirectURI]
65
+ })
66
+ end,
67
+ verify_id_token: normalized[:verify_id_token] || lambda do |token, nonce = nil|
68
+ return false if normalized[:disable_id_token_sign_in]
69
+
70
+ issuers = nil
71
+ unless %w[common organizations consumers].include?(tenant_id.to_s)
72
+ issuers = "#{authority.to_s.sub(%r{/+\z}, "")}/#{tenant_id}/v2.0"
73
+ end
74
+ profile = Base.verify_jwt_with_jwks(
75
+ token,
76
+ jwks: normalized[:jwks],
77
+ jwks_endpoint: normalized[:jwks_endpoint] || "#{authority.to_s.sub(%r{/+\z}, "")}/#{tenant_id}/discovery/v2.0/keys",
78
+ algorithms: ["RS256"],
79
+ issuers: issuers,
80
+ audience: Array(client_id),
81
+ nonce: nonce
82
+ )
83
+
84
+ !!(profile&.fetch("sub", nil) || profile&.fetch("oid", nil))
85
+ end,
86
+ get_user_info: lambda do |tokens|
87
+ custom = normalized[:get_user_info]
88
+ next custom.call(tokens) if custom
89
+
90
+ profile = Base.id_token(tokens) ? Base.decode_jwt_payload(Base.id_token(tokens)) : {}
91
+ profile = Base.get_json("https://graph.microsoft.com/v1.0/me", "Authorization" => "Bearer #{Base.access_token(tokens)}") if profile.empty?
92
+ unless normalized[:disable_profile_photo]
93
+ photo_size = normalized[:profile_photo_size] || 48
94
+ photo = Base.get_bytes(
95
+ "https://graph.microsoft.com/v1.0/me/photos/#{photo_size}x#{photo_size}/$value",
96
+ "Authorization" => "Bearer #{Base.access_token(tokens)}"
97
+ )
98
+ profile["picture"] = "data:image/jpeg;base64, #{Base64.strict_encode64(photo)}" if photo
99
+ end
100
+ email = profile["email"] || profile["mail"] || profile["userPrincipalName"] || profile["preferred_username"]
101
+
102
+ user = Base.apply_profile_mapping(
103
+ {
104
+ id: profile["sub"] || profile["id"] || profile["oid"],
105
+ email: email,
106
+ name: profile["name"] || profile["displayName"],
107
+ image: profile["picture"],
108
+ emailVerified: microsoft_email_verified?(profile, email)
109
+ },
110
+ profile,
111
+ normalized
112
+ )
113
+ {
114
+ user: user,
115
+ data: profile
116
+ }
117
+ end,
118
+ refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
119
+ Base.refresh_access_token(
120
+ "#{base}/token",
121
+ refresh_token,
122
+ client_id: primary_client_id,
123
+ client_secret: client_secret,
124
+ extra_params: {scope: Base.selected_scopes(scopes, normalized, {}).join(" ")}
125
+ )
126
+ end
127
+ }
128
+ end
129
+
130
+ def microsoft_email_verified?(profile, email)
131
+ return !!profile["email_verified"] if profile.key?("email_verified")
132
+
133
+ Array(profile["verified_primary_email"]).include?(email) ||
134
+ Array(profile["verified_secondary_email"]).include?(email)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def naver(client_id:, client_secret:, scopes: ["profile", "email"], **options)
8
+ Base.oauth_provider(
9
+ id: "naver",
10
+ name: "Naver",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://nid.naver.com/oauth2.0/authorize",
14
+ token_endpoint: "https://nid.naver.com/oauth2.0/token",
15
+ user_info_endpoint: "https://openapi.naver.com/v1/nid/me",
16
+ scopes: scopes,
17
+ profile_map: ->(profile) {
18
+ data = profile["response"] || {}
19
+ {
20
+ id: data["id"],
21
+ name: data["name"] || data["nickname"] || "",
22
+ email: data["email"],
23
+ image: data["profile_image"],
24
+ emailVerified: false
25
+ }
26
+ },
27
+ **options
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def notion(client_id:, client_secret:, scopes: [], **options)
8
+ Base.oauth_provider(
9
+ id: "notion",
10
+ name: "Notion",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://api.notion.com/v1/oauth/authorize",
14
+ token_endpoint: "https://api.notion.com/v1/oauth/token",
15
+ user_info_endpoint: "https://api.notion.com/v1/users/me",
16
+ scopes: scopes,
17
+ auth_params: {owner: "user"},
18
+ user_info_headers: {"Notion-Version" => "2022-06-28"},
19
+ profile_map: ->(profile) {
20
+ user = profile.dig("bot", "owner", "user") || profile
21
+ {
22
+ id: user["id"],
23
+ name: user["name"] || "",
24
+ email: user.dig("person", "email"),
25
+ image: user["avatar_url"],
26
+ emailVerified: false
27
+ }
28
+ },
29
+ **options
30
+ )
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def paybin(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
8
+ issuer = (options[:issuer] || "https://idp.paybin.io").to_s.sub(%r{/+\z}, "")
9
+ Base.oauth_provider(
10
+ id: "paybin",
11
+ name: "Paybin",
12
+ client_id: client_id,
13
+ client_secret: client_secret,
14
+ authorization_endpoint: "#{issuer}/oauth2/authorize",
15
+ token_endpoint: "#{issuer}/oauth2/token",
16
+ scopes: scopes,
17
+ pkce: true,
18
+ profile_map: ->(profile) {
19
+ {
20
+ id: profile["sub"],
21
+ name: profile["name"] || profile["preferred_username"] || "",
22
+ email: profile["email"],
23
+ image: profile["picture"],
24
+ emailVerified: !!profile["email_verified"]
25
+ }
26
+ },
27
+ **options
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def paypal(client_id:, client_secret:, scopes: [], **options)
8
+ sandbox = (options[:environment] || "sandbox").to_s == "sandbox"
9
+ auth_host = sandbox ? "https://www.sandbox.paypal.com" : "https://www.paypal.com"
10
+ api_host = sandbox ? "https://api-m.sandbox.paypal.com" : "https://api-m.paypal.com"
11
+ provider = Base.oauth_provider(
12
+ id: "paypal",
13
+ name: "PayPal",
14
+ client_id: client_id,
15
+ client_secret: client_secret,
16
+ authorization_endpoint: "#{auth_host}/signin/authorize",
17
+ token_endpoint: "#{api_host}/v1/oauth2/token",
18
+ user_info_endpoint: "#{api_host}/v1/identity/oauth2/userinfo?schema=paypalv1.1",
19
+ scopes: scopes,
20
+ pkce: true,
21
+ profile_map: ->(profile) {
22
+ {
23
+ id: profile["user_id"],
24
+ name: profile["name"],
25
+ email: profile["email"],
26
+ image: profile["picture"],
27
+ emailVerified: !!profile["email_verified"]
28
+ }
29
+ },
30
+ **options
31
+ )
32
+ 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)["sub"] }
33
+ provider
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def polar(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options)
8
+ Base.oauth_provider(
9
+ id: "polar",
10
+ name: "Polar",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://polar.sh/oauth2/authorize",
14
+ token_endpoint: "https://api.polar.sh/v1/oauth2/token",
15
+ user_info_endpoint: "https://api.polar.sh/v1/oauth2/userinfo",
16
+ scopes: scopes,
17
+ pkce: true,
18
+ profile_map: ->(profile) {
19
+ {
20
+ id: profile["id"],
21
+ name: profile["public_name"] || profile["username"] || "",
22
+ email: profile["email"],
23
+ image: profile["avatar_url"],
24
+ emailVerified: !!profile["email_verified"]
25
+ }
26
+ },
27
+ **options
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def railway(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
8
+ primary_client_id = Base.primary_client_id(client_id)
9
+ credentials = Base64.strict_encode64("#{primary_client_id}:#{client_secret}")
10
+ token_endpoint = options[:token_endpoint] || options[:tokenEndpoint] || "https://backboard.railway.com/oauth/token"
11
+ provider = Base.oauth_provider(
12
+ id: "railway",
13
+ name: "Railway",
14
+ client_id: client_id,
15
+ client_secret: client_secret,
16
+ authorization_endpoint: "https://backboard.railway.com/oauth/auth",
17
+ token_endpoint: "https://backboard.railway.com/oauth/token",
18
+ user_info_endpoint: "https://backboard.railway.com/oauth/me",
19
+ scopes: scopes,
20
+ pkce: true,
21
+ profile_map: ->(profile) {
22
+ {
23
+ id: profile["sub"],
24
+ name: profile["name"],
25
+ email: profile["email"],
26
+ image: profile["picture"],
27
+ emailVerified: false
28
+ }
29
+ },
30
+ **options
31
+ )
32
+ provider[:validate_authorization_code] = lambda do |data|
33
+ Base.post_form_json(token_endpoint, {
34
+ code: data[:code],
35
+ code_verifier: data[:code_verifier] || data[:codeVerifier],
36
+ grant_type: "authorization_code",
37
+ redirect_uri: options[:redirect_uri] || options[:redirectURI] || data[:redirect_uri] || data[:redirectURI]
38
+ }, {"Authorization" => "Basic #{credentials}"})
39
+ end
40
+ provider[:refresh_access_token] = options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
41
+ Base.normalize_tokens(Base.post_form_json(token_endpoint, {
42
+ grant_type: "refresh_token",
43
+ refresh_token: refresh_token
44
+ }, {"Authorization" => "Basic #{credentials}"}))
45
+ end
46
+ provider
47
+ end
48
+ end
49
+ end