better_auth 0.2.0 → 0.4.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +5 -3
  4. data/lib/better_auth/adapters/internal_adapter.rb +173 -20
  5. data/lib/better_auth/adapters/memory.rb +61 -12
  6. data/lib/better_auth/adapters/mongodb.rb +5 -365
  7. data/lib/better_auth/adapters/sql.rb +44 -3
  8. data/lib/better_auth/api.rb +7 -2
  9. data/lib/better_auth/async.rb +70 -0
  10. data/lib/better_auth/context.rb +2 -1
  11. data/lib/better_auth/database_hooks.rb +3 -3
  12. data/lib/better_auth/deprecate.rb +28 -0
  13. data/lib/better_auth/endpoint.rb +5 -2
  14. data/lib/better_auth/host.rb +166 -0
  15. data/lib/better_auth/instrumentation.rb +74 -0
  16. data/lib/better_auth/logger.rb +31 -0
  17. data/lib/better_auth/middleware/origin_check.rb +2 -2
  18. data/lib/better_auth/oauth2.rb +94 -0
  19. data/lib/better_auth/plugin.rb +14 -1
  20. data/lib/better_auth/plugins/email_otp.rb +16 -5
  21. data/lib/better_auth/plugins/generic_oauth.rb +14 -28
  22. data/lib/better_auth/plugins/oauth_protocol.rb +553 -64
  23. data/lib/better_auth/plugins/organization/schema.rb +6 -0
  24. data/lib/better_auth/plugins/organization.rb +56 -20
  25. data/lib/better_auth/plugins/two_factor.rb +53 -18
  26. data/lib/better_auth/rate_limiter.rb +37 -2
  27. data/lib/better_auth/request_state.rb +44 -0
  28. data/lib/better_auth/router.rb +14 -1
  29. data/lib/better_auth/routes/account.rb +16 -4
  30. data/lib/better_auth/routes/email_verification.rb +5 -2
  31. data/lib/better_auth/routes/password.rb +21 -1
  32. data/lib/better_auth/routes/session.rb +27 -4
  33. data/lib/better_auth/routes/sign_in.rb +3 -1
  34. data/lib/better_auth/routes/sign_up.rb +60 -1
  35. data/lib/better_auth/routes/social.rb +231 -22
  36. data/lib/better_auth/routes/user.rb +23 -5
  37. data/lib/better_auth/schema/sql.rb +11 -0
  38. data/lib/better_auth/schema.rb +16 -0
  39. data/lib/better_auth/session.rb +12 -1
  40. data/lib/better_auth/social_providers/apple.rb +44 -8
  41. data/lib/better_auth/social_providers/atlassian.rb +32 -0
  42. data/lib/better_auth/social_providers/base.rb +262 -4
  43. data/lib/better_auth/social_providers/cognito.rb +32 -0
  44. data/lib/better_auth/social_providers/discord.rb +27 -5
  45. data/lib/better_auth/social_providers/dropbox.rb +33 -0
  46. data/lib/better_auth/social_providers/facebook.rb +35 -0
  47. data/lib/better_auth/social_providers/figma.rb +31 -0
  48. data/lib/better_auth/social_providers/github.rb +21 -6
  49. data/lib/better_auth/social_providers/gitlab.rb +16 -3
  50. data/lib/better_auth/social_providers/google.rb +38 -13
  51. data/lib/better_auth/social_providers/huggingface.rb +31 -0
  52. data/lib/better_auth/social_providers/kakao.rb +32 -0
  53. data/lib/better_auth/social_providers/kick.rb +32 -0
  54. data/lib/better_auth/social_providers/line.rb +33 -0
  55. data/lib/better_auth/social_providers/linear.rb +44 -0
  56. data/lib/better_auth/social_providers/linkedin.rb +30 -0
  57. data/lib/better_auth/social_providers/microsoft_entra_id.rb +79 -7
  58. data/lib/better_auth/social_providers/naver.rb +31 -0
  59. data/lib/better_auth/social_providers/notion.rb +33 -0
  60. data/lib/better_auth/social_providers/paybin.rb +31 -0
  61. data/lib/better_auth/social_providers/paypal.rb +36 -0
  62. data/lib/better_auth/social_providers/polar.rb +31 -0
  63. data/lib/better_auth/social_providers/railway.rb +49 -0
  64. data/lib/better_auth/social_providers/reddit.rb +32 -0
  65. data/lib/better_auth/social_providers/roblox.rb +31 -0
  66. data/lib/better_auth/social_providers/salesforce.rb +38 -0
  67. data/lib/better_auth/social_providers/slack.rb +30 -0
  68. data/lib/better_auth/social_providers/spotify.rb +31 -0
  69. data/lib/better_auth/social_providers/tiktok.rb +35 -0
  70. data/lib/better_auth/social_providers/twitch.rb +39 -0
  71. data/lib/better_auth/social_providers/twitter.rb +32 -0
  72. data/lib/better_auth/social_providers/vercel.rb +47 -0
  73. data/lib/better_auth/social_providers/vk.rb +34 -0
  74. data/lib/better_auth/social_providers/wechat.rb +104 -0
  75. data/lib/better_auth/social_providers/zoom.rb +31 -0
  76. data/lib/better_auth/social_providers.rb +29 -0
  77. data/lib/better_auth/url_helpers.rb +195 -0
  78. data/lib/better_auth/version.rb +1 -1
  79. data/lib/better_auth.rb +8 -1
  80. metadata +38 -15
@@ -5,13 +5,14 @@ module BetterAuth
5
5
  module_function
6
6
 
7
7
  def discord(client_id:, client_secret:, scopes: ["identify", "email"], **options)
8
+ normalized = Base.normalize_options(options)
8
9
  {
9
10
  id: "discord",
10
11
  name: "Discord",
11
12
  client_id: client_id,
12
13
  client_secret: client_secret,
13
14
  create_authorization_url: lambda do |data|
14
- selected_scopes = data[:scopes] || scopes
15
+ selected_scopes = Base.selected_scopes(scopes, normalized, data)
15
16
  params = {
16
17
  client_id: client_id,
17
18
  redirect_uri: data[:redirect_uri] || data[:redirectURI],
@@ -33,24 +34,45 @@ module BetterAuth
33
34
  })
34
35
  end,
35
36
  get_user_info: lambda do |tokens|
37
+ custom = normalized[:get_user_info]
38
+ next custom.call(tokens) if custom
39
+
36
40
  profile = Base.get_json("https://discord.com/api/users/@me", "Authorization" => "Bearer #{Base.access_token(tokens)}")
37
- {
38
- user: {
41
+ image = discord_avatar_url(profile)
42
+ profile["image_url"] = image
43
+ user = Base.apply_profile_mapping(
44
+ {
39
45
  id: profile["id"],
40
46
  email: profile["email"],
41
47
  name: profile["global_name"] || profile["username"] || "",
42
- image: discord_avatar_url(profile),
48
+ image: image,
43
49
  emailVerified: !!profile["verified"]
44
50
  },
51
+ profile,
52
+ normalized
53
+ )
54
+ {
55
+ user: user,
45
56
  data: profile
46
57
  }
58
+ end,
59
+ refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
60
+ Base.refresh_access_token("https://discord.com/api/oauth2/token", refresh_token, client_id: client_id, client_secret: client_secret)
47
61
  end
48
62
  }
49
63
  end
50
64
 
51
65
  def discord_avatar_url(profile)
52
66
  avatar = profile["avatar"]
53
- return nil unless avatar
67
+ unless avatar
68
+ discriminator = profile["discriminator"].to_s
69
+ default_avatar_number = if discriminator == "0"
70
+ (profile["id"].to_i >> 22) % 6
71
+ else
72
+ discriminator.to_i % 5
73
+ end
74
+ return "https://cdn.discordapp.com/embed/avatars/#{default_avatar_number}.png"
75
+ end
54
76
 
55
77
  format = avatar.start_with?("a_") ? "gif" : "png"
56
78
  "https://cdn.discordapp.com/avatars/#{profile["id"]}/#{avatar}.#{format}"
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def dropbox(client_id:, client_secret:, scopes: ["account_info.read"], **options)
8
+ Base.oauth_provider(
9
+ id: "dropbox",
10
+ name: "Dropbox",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://www.dropbox.com/oauth2/authorize",
14
+ token_endpoint: "https://api.dropboxapi.com/oauth2/token",
15
+ user_info_endpoint: "https://api.dropboxapi.com/2/users/get_current_account",
16
+ user_info_method: :post,
17
+ scopes: scopes,
18
+ pkce: true,
19
+ auth_params: ->(_data, opts) { {token_access_type: opts[:access_type] || opts[:accessType]} },
20
+ profile_map: ->(profile) {
21
+ {
22
+ id: profile["account_id"],
23
+ name: profile.dig("name", "display_name"),
24
+ email: profile["email"],
25
+ image: profile["profile_photo_url"],
26
+ emailVerified: !!profile["email_verified"]
27
+ }
28
+ },
29
+ **options
30
+ )
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def facebook(client_id:, client_secret:, scopes: ["email", "public_profile"], **options)
8
+ fields = Array(options[:fields] || %w[id name email picture email_verified]).join(",")
9
+ provider = Base.oauth_provider(
10
+ id: "facebook",
11
+ name: "Facebook",
12
+ client_id: client_id,
13
+ client_secret: client_secret,
14
+ authorization_endpoint: "https://www.facebook.com/v24.0/dialog/oauth",
15
+ token_endpoint: "https://graph.facebook.com/v24.0/oauth/access_token",
16
+ user_info_endpoint: "https://graph.facebook.com/me?fields=#{URI.encode_www_form_component(fields)}",
17
+ scopes: scopes,
18
+ auth_params: ->(_data, opts) { {config_id: opts[:config_id] || opts[:configId]} },
19
+ profile_map: ->(profile) {
20
+ picture = profile.dig("picture", "data", "url") || profile["picture"]
21
+ {
22
+ id: profile["id"] || profile["sub"],
23
+ name: profile["name"],
24
+ email: profile["email"],
25
+ image: picture,
26
+ emailVerified: !!profile["email_verified"]
27
+ }
28
+ },
29
+ **options
30
+ )
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
+ provider
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module SocialProviders
5
+ module_function
6
+
7
+ def figma(client_id:, client_secret:, scopes: ["current_user:read"], **options)
8
+ Base.oauth_provider(
9
+ id: "figma",
10
+ name: "Figma",
11
+ client_id: client_id,
12
+ client_secret: client_secret,
13
+ authorization_endpoint: "https://www.figma.com/oauth",
14
+ token_endpoint: "https://api.figma.com/v1/oauth/token",
15
+ user_info_endpoint: "https://api.figma.com/v1/me",
16
+ scopes: scopes,
17
+ pkce: true,
18
+ profile_map: ->(profile) {
19
+ {
20
+ id: profile["id"],
21
+ name: profile["handle"],
22
+ email: profile["email"],
23
+ image: profile["img_url"],
24
+ emailVerified: false
25
+ }
26
+ },
27
+ **options
28
+ )
29
+ end
30
+ end
31
+ end
@@ -5,6 +5,10 @@ module BetterAuth
5
5
  module_function
6
6
 
7
7
  def github(client_id:, client_secret:, scopes: ["read:user", "user:email"], **options)
8
+ normalized = Base.normalize_options(options)
9
+ token_endpoint = normalized[:token_endpoint] || "https://github.com/login/oauth/access_token"
10
+ user_info_endpoint = normalized[:user_info_endpoint] || "https://api.github.com/user"
11
+ emails_endpoint = normalized[:emails_endpoint] || "https://api.github.com/user/emails"
8
12
  {
9
13
  id: "github",
10
14
  name: "GitHub",
@@ -14,14 +18,14 @@ module BetterAuth
14
18
  Base.authorization_url(options[:authorization_endpoint] || "https://github.com/login/oauth/authorize", {
15
19
  client_id: client_id,
16
20
  redirect_uri: data[:redirect_uri] || data[:redirectURI],
17
- scope: data[:scopes] || scopes,
21
+ scope: Base.selected_scopes(scopes, normalized, data),
18
22
  state: data[:state],
19
23
  login_hint: data[:loginHint] || data[:login_hint],
20
24
  prompt: options[:prompt]
21
25
  })
22
26
  end,
23
27
  validate_authorization_code: lambda do |data|
24
- Base.post_form("https://github.com/login/oauth/access_token", {
28
+ Base.post_form(token_endpoint, {
25
29
  client_id: client_id,
26
30
  client_secret: client_secret,
27
31
  code: data[:code],
@@ -30,28 +34,39 @@ module BetterAuth
30
34
  })
31
35
  end,
32
36
  get_user_info: lambda do |tokens|
37
+ custom = normalized[:get_user_info]
38
+ next custom.call(tokens) if custom
39
+
33
40
  headers = {
34
41
  "Authorization" => "Bearer #{Base.access_token(tokens)}",
35
42
  "Accept" => "application/json",
36
43
  "User-Agent" => "better-auth"
37
44
  }
38
- profile = Base.get_json("https://api.github.com/user", headers)
39
- emails = Base.get_json("https://api.github.com/user/emails", headers)
45
+ profile = Base.get_json(user_info_endpoint, headers)
46
+ emails = Base.get_json(emails_endpoint, headers)
40
47
  primary = Array(emails).find { |email| email["email"] == profile["email"] } ||
41
48
  Array(emails).find { |email| email["primary"] } ||
42
49
  Array(emails).first ||
43
50
  {}
44
51
 
45
- {
46
- user: {
52
+ user = Base.apply_profile_mapping(
53
+ {
47
54
  id: profile["id"].to_s,
48
55
  email: profile["email"] || primary["email"],
49
56
  name: profile["name"] || profile["login"],
50
57
  image: profile["avatar_url"],
51
58
  emailVerified: !!primary["verified"]
52
59
  },
60
+ profile,
61
+ normalized
62
+ )
63
+ {
64
+ user: user,
53
65
  data: profile
54
66
  }
67
+ end,
68
+ refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
69
+ Base.refresh_access_token(token_endpoint, refresh_token, client_id: client_id, client_secret: client_secret)
55
70
  end
56
71
  }
57
72
  end
@@ -6,6 +6,7 @@ module BetterAuth
6
6
 
7
7
  def gitlab(client_id:, client_secret:, issuer: "https://gitlab.com", scopes: ["read_user"], **options)
8
8
  base = issuer.to_s.sub(%r{/+\z}, "")
9
+ normalized = Base.normalize_options(options)
9
10
  {
10
11
  id: "gitlab",
11
12
  name: "GitLab",
@@ -16,7 +17,7 @@ module BetterAuth
16
17
  client_id: client_id,
17
18
  redirect_uri: data[:redirect_uri] || data[:redirectURI],
18
19
  response_type: "code",
19
- scope: data[:scopes] || scopes,
20
+ scope: Base.selected_scopes(scopes, normalized, data),
20
21
  state: data[:state],
21
22
  code_challenge: (data[:code_verifier] || data[:codeVerifier]) && Base.pkce_challenge(data[:code_verifier] || data[:codeVerifier]),
22
23
  code_challenge_method: (data[:code_verifier] || data[:codeVerifier]) && "S256",
@@ -34,19 +35,31 @@ module BetterAuth
34
35
  })
35
36
  end,
36
37
  get_user_info: lambda do |tokens|
38
+ custom = normalized[:get_user_info]
39
+ next custom.call(tokens) if custom
40
+
37
41
  profile = Base.get_json("#{base}/api/v4/user", "Authorization" => "Bearer #{Base.access_token(tokens)}")
38
42
  return nil if profile["state"] && profile["state"] != "active"
43
+ return nil if profile["locked"] == true
39
44
 
40
- {
41
- user: {
45
+ user = Base.apply_profile_mapping(
46
+ {
42
47
  id: profile["id"].to_s,
43
48
  email: profile["email"],
44
49
  name: profile["name"] || profile["username"],
45
50
  image: profile["avatar_url"],
46
51
  emailVerified: !!profile["email_verified"]
47
52
  },
53
+ profile,
54
+ normalized
55
+ )
56
+ {
57
+ user: user,
48
58
  data: profile
49
59
  }
60
+ end,
61
+ refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
62
+ Base.refresh_access_token("#{base}/oauth/token", refresh_token, client_id: client_id, client_secret: client_secret)
50
63
  end
51
64
  }
52
65
  end
@@ -5,6 +5,8 @@ module BetterAuth
5
5
  module_function
6
6
 
7
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)
8
10
  {
9
11
  id: "google",
10
12
  name: "Google",
@@ -12,11 +14,13 @@ module BetterAuth
12
14
  client_secret: client_secret,
13
15
  create_authorization_url: lambda do |data|
14
16
  verifier = data[:code_verifier] || data[:codeVerifier]
17
+ raise Error, "codeVerifier is required for Google" if verifier.to_s.empty?
18
+
15
19
  Base.authorization_url(options[:authorization_endpoint] || "https://accounts.google.com/o/oauth2/v2/auth", {
16
- client_id: client_id,
20
+ client_id: primary_client_id,
17
21
  redirect_uri: data[:redirect_uri] || data[:redirectURI],
18
22
  response_type: "code",
19
- scope: data[:scopes] || scopes,
23
+ scope: Base.selected_scopes(scopes, normalized, data),
20
24
  state: data[:state],
21
25
  code_challenge: verifier && Base.pkce_challenge(verifier),
22
26
  code_challenge_method: verifier && "S256",
@@ -30,7 +34,7 @@ module BetterAuth
30
34
  end,
31
35
  validate_authorization_code: lambda do |data|
32
36
  Base.post_form("https://oauth2.googleapis.com/token", {
33
- client_id: client_id,
37
+ client_id: primary_client_id,
34
38
  client_secret: client_secret,
35
39
  code: data[:code],
36
40
  code_verifier: data[:code_verifier] || data[:codeVerifier],
@@ -38,26 +42,47 @@ module BetterAuth
38
42
  redirect_uri: data[:redirect_uri] || data[:redirectURI]
39
43
  })
40
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,
41
62
  get_user_info: lambda do |tokens|
42
- profile = if Base.id_token(tokens)
43
- Base.decode_jwt_payload(Base.id_token(tokens))
44
- else
45
- Base.get_json(
46
- "https://openidconnect.googleapis.com/v1/userinfo",
47
- "Authorization" => "Bearer #{Base.access_token(tokens)}"
48
- )
49
- end
63
+ custom = normalized[:get_user_info]
64
+ next custom.call(tokens) if custom
65
+ next nil unless Base.id_token(tokens)
50
66
 
51
- {
52
- user: {
67
+ profile = Base.decode_jwt_payload(Base.id_token(tokens))
68
+ user = Base.apply_profile_mapping(
69
+ {
53
70
  id: profile["sub"],
54
71
  email: profile["email"],
55
72
  name: profile["name"],
56
73
  image: profile["picture"],
57
74
  emailVerified: !!profile["email_verified"]
58
75
  },
76
+ profile,
77
+ normalized
78
+ )
79
+ {
80
+ user: user,
59
81
  data: profile
60
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)
61
86
  end
62
87
  }
63
88
  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