Module: BetterAuth::SocialProviders

Defined in:
lib/better_auth/social_providers/vk.rb,
lib/better_auth/social_providers/base.rb,
lib/better_auth/social_providers/kick.rb,
lib/better_auth/social_providers/line.rb,
lib/better_auth/social_providers/zoom.rb,
lib/better_auth/social_providers/apple.rb,
lib/better_auth/social_providers/figma.rb,
lib/better_auth/social_providers/kakao.rb,
lib/better_auth/social_providers/naver.rb,
lib/better_auth/social_providers/polar.rb,
lib/better_auth/social_providers/slack.rb,
lib/better_auth/social_providers/github.rb,
lib/better_auth/social_providers/gitlab.rb,
lib/better_auth/social_providers/google.rb,
lib/better_auth/social_providers/linear.rb,
lib/better_auth/social_providers/notion.rb,
lib/better_auth/social_providers/paybin.rb,
lib/better_auth/social_providers/paypal.rb,
lib/better_auth/social_providers/reddit.rb,
lib/better_auth/social_providers/roblox.rb,
lib/better_auth/social_providers/tiktok.rb,
lib/better_auth/social_providers/twitch.rb,
lib/better_auth/social_providers/vercel.rb,
lib/better_auth/social_providers/wechat.rb,
lib/better_auth/social_providers/cognito.rb,
lib/better_auth/social_providers/discord.rb,
lib/better_auth/social_providers/dropbox.rb,
lib/better_auth/social_providers/railway.rb,
lib/better_auth/social_providers/spotify.rb,
lib/better_auth/social_providers/twitter.rb,
lib/better_auth/social_providers/facebook.rb,
lib/better_auth/social_providers/linkedin.rb,
lib/better_auth/social_providers/atlassian.rb,
lib/better_auth/social_providers/salesforce.rb,
lib/better_auth/social_providers/huggingface.rb,
lib/better_auth/social_providers/microsoft_entra_id.rb

Defined Under Namespace

Modules: Base

Class Method Summary collapse

Class Method Details

.apple(client_id:, client_secret:, scopes: ["email", "name"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/better_auth/social_providers/apple.rb', line 7

def apple(client_id:, client_secret:, scopes: ["email", "name"], **options)
  normalized = Base.normalize_options(options)
  primary_client_id = Base.primary_client_id(client_id)
  {
    id: "apple",
    name: "Apple",
    client_id: client_id,
    client_secret: client_secret,
    create_authorization_url: lambda do |data|
      Base.authorization_url(options[:authorization_endpoint] || "https://appleid.apple.com/auth/authorize", {
        client_id: primary_client_id,
        redirect_uri: data[:redirect_uri] || data[:redirectURI],
        response_type: "code id_token",
        response_mode: options[:response_mode] || options[:responseMode] || "form_post",
        scope: Base.selected_scopes(scopes, normalized, data),
        state: data[:state]
      })
    end,
    validate_authorization_code: lambda do |data|
      Base.post_form("https://appleid.apple.com/auth/token", {
        client_id: primary_client_id,
        client_secret: client_secret,
        code: data[:code],
        code_verifier: data[:code_verifier] || data[:codeVerifier],
        grant_type: "authorization_code",
        redirect_uri: data[:redirect_uri] || data[:redirectURI]
      })
    end,
    verify_id_token: normalized[:verify_id_token] || lambda do |token, nonce = nil|
      return false if normalized[:disable_id_token_sign_in]

      audiences = Array(normalized[:audience] || normalized[:app_bundle_identifier] || normalized[:appBundleIdentifier] || client_id)
      return false if audiences.empty?

      profile = Base.verify_jwt_with_jwks(
        token,
        jwks: normalized[:jwks],
        jwks_endpoint: normalized[:jwks_endpoint] || "https://appleid.apple.com/auth/keys",
        algorithms: ["RS256"],
        issuers: "https://appleid.apple.com",
        audience: audiences,
        nonce: nonce
      )
      !!profile&.fetch("sub", nil)
    end,
    get_user_info: lambda do |tokens|
      custom = normalized[:get_user_info]
      next custom.call(tokens) if custom

      profile = Base.decode_jwt_payload(Base.id_token(tokens))
      apple_user = tokens[:user] || tokens["user"] || {}
      name = apple_user.dig(:name, :firstName) ||
        apple_user.dig(:name, :first_name) ||
        apple_user.dig("name", "firstName") ||
        apple_user.dig("name", "first_name")
      last_name = apple_user.dig(:name, :lastName) ||
        apple_user.dig(:name, :last_name) ||
        apple_user.dig("name", "lastName") ||
        apple_user.dig("name", "last_name")
      full_name = [name, last_name].compact.join(" ").strip
      full_name = profile["name"] || "" if full_name.empty?

      user = Base.apply_profile_mapping(
        {
          id: profile["sub"],
          email: profile["email"],
          name: full_name,
          image: profile["picture"],
          emailVerified: profile["email_verified"] == true || profile["email_verified"] == "true"
        },
        profile.merge("name" => full_name),
        normalized
      )
      {
        user: user,
        data: profile.merge("name" => full_name)
      }
    end,
    refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
      Base.refresh_access_token("https://appleid.apple.com/auth/token", refresh_token, client_id: primary_client_id, client_secret: client_secret)
    end
  }
end

.atlassian(client_id:, client_secret:, scopes: ["read:jira-user", "offline_access"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/better_auth/social_providers/atlassian.rb', line 7

def atlassian(client_id:, client_secret:, scopes: ["read:jira-user", "offline_access"], **options)
  Base.oauth_provider(
    id: "atlassian",
    name: "Atlassian",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://auth.atlassian.com/authorize",
    token_endpoint: "https://auth.atlassian.com/oauth/token",
    user_info_endpoint: "https://api.atlassian.com/me",
    scopes: scopes,
    pkce: true,
    auth_params: {audience: "api.atlassian.com"},
    profile_map: ->(profile) {
      {
        id: profile["account_id"],
        name: profile["name"],
        email: profile["email"],
        image: profile["picture"],
        emailVerified: false
      }
    },
    **options
  )
end

.cognito(client_id:, client_secret: nil, scopes: ["openid", "profile", "email"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/better_auth/social_providers/cognito.rb', line 7

def cognito(client_id:, client_secret: nil, scopes: ["openid", "profile", "email"], **options)
  domain = (options[:domain] || options[:issuer] || "https://cognito-idp.#{options[:region] || "us-east-1"}.amazonaws.com").to_s.sub(%r{/+\z}, "")
  Base.oauth_provider(
    id: "cognito",
    name: "Cognito",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "#{domain}/oauth2/authorize",
    token_endpoint: "#{domain}/oauth2/token",
    user_info_endpoint: "#{domain}/oauth2/userinfo",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["sub"],
        name: profile["name"] || profile["given_name"] || profile["username"] || "",
        email: profile["email"],
        image: profile["picture"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
end

.discord(client_id:, client_secret:, scopes: ["identify", "email"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/better_auth/social_providers/discord.rb', line 7

def discord(client_id:, client_secret:, scopes: ["identify", "email"], **options)
  normalized = Base.normalize_options(options)
  {
    id: "discord",
    name: "Discord",
    client_id: client_id,
    client_secret: client_secret,
    create_authorization_url: lambda do |data|
      selected_scopes = Base.selected_scopes(scopes, normalized, data)
      params = {
        client_id: client_id,
        redirect_uri: data[:redirect_uri] || data[:redirectURI],
        response_type: "code",
        scope: selected_scopes,
        state: data[:state],
        prompt: options.fetch(:prompt, "none")
      }
      params[:permissions] = options[:permissions] if selected_scopes.include?("bot") && options.key?(:permissions)
      Base.authorization_url("https://discord.com/api/oauth2/authorize", params)
    end,
    validate_authorization_code: lambda do |data|
      Base.post_form("https://discord.com/api/oauth2/token", {
        client_id: client_id,
        client_secret: client_secret,
        code: data[:code],
        grant_type: "authorization_code",
        redirect_uri: data[:redirect_uri] || data[:redirectURI]
      })
    end,
    get_user_info: lambda do |tokens|
      custom = normalized[:get_user_info]
      next custom.call(tokens) if custom

      profile = Base.get_json("https://discord.com/api/users/@me", "Authorization" => "Bearer #{Base.access_token(tokens)}")
      image = discord_avatar_url(profile)
      profile["image_url"] = image
      user = Base.apply_profile_mapping(
        {
          id: profile["id"],
          email: profile["email"],
          name: profile["global_name"] || profile["username"] || "",
          image: image,
          emailVerified: !!profile["verified"]
        },
        profile,
        normalized
      )
      {
        user: user,
        data: profile
      }
    end,
    refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
      Base.refresh_access_token("https://discord.com/api/oauth2/token", refresh_token, client_id: client_id, client_secret: client_secret)
    end
  }
end

.discord_avatar_url(profile) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/better_auth/social_providers/discord.rb', line 65

def discord_avatar_url(profile)
  avatar = profile["avatar"]
  unless avatar
    discriminator = profile["discriminator"].to_s
    default_avatar_number = if discriminator == "0"
      (profile["id"].to_i >> 22) % 6
    else
      discriminator.to_i % 5
    end
    return "https://cdn.discordapp.com/embed/avatars/#{default_avatar_number}.png"
  end

  format = avatar.start_with?("a_") ? "gif" : "png"
  "https://cdn.discordapp.com/avatars/#{profile["id"]}/#{avatar}.#{format}"
end

.dropbox(client_id:, client_secret:, scopes: ["account_info.read"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/better_auth/social_providers/dropbox.rb', line 7

def dropbox(client_id:, client_secret:, scopes: ["account_info.read"], **options)
  Base.oauth_provider(
    id: "dropbox",
    name: "Dropbox",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://www.dropbox.com/oauth2/authorize",
    token_endpoint: "https://api.dropboxapi.com/oauth2/token",
    user_info_endpoint: "https://api.dropboxapi.com/2/users/get_current_account",
    user_info_method: :post,
    scopes: scopes,
    pkce: true,
    auth_params: ->(_data, opts) { {token_access_type: opts[:access_type] || opts[:accessType]} },
    profile_map: ->(profile) {
      {
        id: profile["account_id"],
        name: profile.dig("name", "display_name"),
        email: profile["email"],
        image: profile["profile_photo_url"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
end

.facebook(client_id:, client_secret:, scopes: ["email", "public_profile"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/better_auth/social_providers/facebook.rb', line 7

def facebook(client_id:, client_secret:, scopes: ["email", "public_profile"], **options)
  fields = Array(options[:fields] || %w[id name email picture email_verified]).join(",")
  provider = Base.oauth_provider(
    id: "facebook",
    name: "Facebook",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://www.facebook.com/v24.0/dialog/oauth",
    token_endpoint: "https://graph.facebook.com/v24.0/oauth/access_token",
    user_info_endpoint: "https://graph.facebook.com/me?fields=#{URI.encode_www_form_component(fields)}",
    scopes: scopes,
    auth_params: ->(_data, opts) { {config_id: opts[:config_id] || opts[:configId]} },
    profile_map: ->(profile) {
      picture = profile.dig("picture", "data", "url") || profile["picture"]
      {
        id: profile["id"] || profile["sub"],
        name: profile["name"],
        email: profile["email"],
        image: picture,
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
  provider[:verify_id_token] = provider[:options][:verify_id_token] || ->(token, _nonce = nil) { provider[:options][:disable_id_token_sign_in] ? false : !token.to_s.empty? }
  provider
end

.figma(client_id:, client_secret:, scopes: ["current_user:read"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/better_auth/social_providers/figma.rb', line 7

def figma(client_id:, client_secret:, scopes: ["current_user:read"], **options)
  Base.oauth_provider(
    id: "figma",
    name: "Figma",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://www.figma.com/oauth",
    token_endpoint: "https://api.figma.com/v1/oauth/token",
    user_info_endpoint: "https://api.figma.com/v1/me",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["id"],
        name: profile["handle"],
        email: profile["email"],
        image: profile["img_url"],
        emailVerified: false
      }
    },
    **options
  )
end

.github(client_id:, client_secret:, scopes: ["read:user", "user:email"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/better_auth/social_providers/github.rb', line 7

def github(client_id:, client_secret:, scopes: ["read:user", "user:email"], **options)
  normalized = Base.normalize_options(options)
  token_endpoint = normalized[:token_endpoint] || "https://github.com/login/oauth/access_token"
   = normalized[:user_info_endpoint] || "https://api.github.com/user"
  emails_endpoint = normalized[:emails_endpoint] || "https://api.github.com/user/emails"
  {
    id: "github",
    name: "GitHub",
    client_id: client_id,
    client_secret: client_secret,
    create_authorization_url: lambda do |data|
      Base.authorization_url(options[:authorization_endpoint] || "https://github.com/login/oauth/authorize", {
        client_id: client_id,
        redirect_uri: data[:redirect_uri] || data[:redirectURI],
        scope: Base.selected_scopes(scopes, normalized, data),
        state: data[:state],
        login_hint: data[:loginHint] || data[:login_hint],
        prompt: options[:prompt]
      })
    end,
    validate_authorization_code: lambda do |data|
      Base.post_form(token_endpoint, {
        client_id: client_id,
        client_secret: client_secret,
        code: data[:code],
        code_verifier: data[:code_verifier] || data[:codeVerifier],
        redirect_uri: data[:redirect_uri] || data[:redirectURI]
      })
    end,
    get_user_info: lambda do |tokens|
      custom = normalized[:get_user_info]
      next custom.call(tokens) if custom

      headers = {
        "Authorization" => "Bearer #{Base.access_token(tokens)}",
        "Accept" => "application/json",
        "User-Agent" => "better-auth"
      }
      profile = Base.get_json(, headers)
      emails = Base.get_json(emails_endpoint, headers)
      primary = Array(emails).find { |email| email["email"] == profile["email"] } ||
        Array(emails).find { |email| email["primary"] } ||
        Array(emails).first ||
        {}

      user = Base.apply_profile_mapping(
        {
          id: profile["id"].to_s,
          email: profile["email"] || primary["email"],
          name: profile["name"] || profile["login"],
          image: profile["avatar_url"],
          emailVerified: !!primary["verified"]
        },
        profile,
        normalized
      )
      {
        user: user,
        data: profile
      }
    end,
    refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
      Base.refresh_access_token(token_endpoint, refresh_token, client_id: client_id, client_secret: client_secret)
    end
  }
end

.gitlab(client_id:, client_secret:, issuer: "https://gitlab.com", scopes: ["read_user"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/better_auth/social_providers/gitlab.rb', line 7

def gitlab(client_id:, client_secret:, issuer: "https://gitlab.com", scopes: ["read_user"], **options)
  base = issuer.to_s.sub(%r{/+\z}, "")
  normalized = Base.normalize_options(options)
  {
    id: "gitlab",
    name: "GitLab",
    client_id: client_id,
    client_secret: client_secret,
    create_authorization_url: lambda do |data|
      Base.authorization_url(options[:authorization_endpoint] || "#{base}/oauth/authorize", {
        client_id: client_id,
        redirect_uri: data[:redirect_uri] || data[:redirectURI],
        response_type: "code",
        scope: Base.selected_scopes(scopes, normalized, data),
        state: data[:state],
        code_challenge: (data[:code_verifier] || data[:codeVerifier]) && Base.pkce_challenge(data[:code_verifier] || data[:codeVerifier]),
        code_challenge_method: (data[:code_verifier] || data[:codeVerifier]) && "S256",
        login_hint: data[:loginHint] || data[:login_hint]
      })
    end,
    validate_authorization_code: lambda do |data|
      Base.post_form("#{base}/oauth/token", {
        client_id: client_id,
        client_secret: client_secret,
        code: data[:code],
        code_verifier: data[:code_verifier] || data[:codeVerifier],
        grant_type: "authorization_code",
        redirect_uri: data[:redirect_uri] || data[:redirectURI]
      })
    end,
    get_user_info: lambda do |tokens|
      custom = normalized[:get_user_info]
      next custom.call(tokens) if custom

      profile = Base.get_json("#{base}/api/v4/user", "Authorization" => "Bearer #{Base.access_token(tokens)}")
      return nil if profile["state"] && profile["state"] != "active"
      return nil if profile["locked"] == true

      user = Base.apply_profile_mapping(
        {
          id: profile["id"].to_s,
          email: profile["email"],
          name: profile["name"] || profile["username"],
          image: profile["avatar_url"],
          emailVerified: !!profile["email_verified"]
        },
        profile,
        normalized
      )
      {
        user: user,
        data: profile
      }
    end,
    refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
      Base.refresh_access_token("#{base}/oauth/token", refresh_token, client_id: client_id, client_secret: client_secret)
    end
  }
end

.google(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/better_auth/social_providers/google.rb', line 7

def google(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
  normalized = Base.normalize_options(options)
  primary_client_id = Base.primary_client_id(client_id)
  {
    id: "google",
    name: "Google",
    client_id: client_id,
    client_secret: client_secret,
    create_authorization_url: lambda do |data|
      verifier = data[:code_verifier] || data[:codeVerifier]
      raise Error, "codeVerifier is required for Google" if verifier.to_s.empty?

      Base.authorization_url(options[:authorization_endpoint] || "https://accounts.google.com/o/oauth2/v2/auth", {
        client_id: primary_client_id,
        redirect_uri: data[:redirect_uri] || data[:redirectURI],
        response_type: "code",
        scope: Base.selected_scopes(scopes, normalized, data),
        state: data[:state],
        code_challenge: verifier && Base.pkce_challenge(verifier),
        code_challenge_method: verifier && "S256",
        login_hint: data[:loginHint] || data[:login_hint],
        prompt: options[:prompt],
        access_type: options[:access_type] || options[:accessType] || "offline",
        display: data[:display] || options[:display],
        hd: options[:hd],
        include_granted_scopes: "true"
      })
    end,
    validate_authorization_code: lambda do |data|
      Base.post_form("https://oauth2.googleapis.com/token", {
        client_id: primary_client_id,
        client_secret: client_secret,
        code: data[:code],
        code_verifier: data[:code_verifier] || data[:codeVerifier],
        grant_type: "authorization_code",
        redirect_uri: data[:redirect_uri] || data[:redirectURI]
      })
    end,
    verify_id_token: normalized[:verify_id_token] || lambda do |token, nonce = nil|
      return false if normalized[:disable_id_token_sign_in]

      audiences = Array(client_id)
      return false if audiences.empty?

      profile = Base.verify_jwt_with_jwks(
        token,
        jwks: normalized[:jwks],
        jwks_endpoint: normalized[:jwks_endpoint] || "https://www.googleapis.com/oauth2/v3/certs",
        algorithms: ["RS256"],
        issuers: ["https://accounts.google.com", "accounts.google.com"],
        audience: audiences,
        nonce: nonce
      )
      !!profile&.fetch("sub", nil)
    end,
    get_user_info: lambda do |tokens|
      custom = normalized[:get_user_info]
      next custom.call(tokens) if custom
      next nil unless Base.id_token(tokens)

      profile = Base.decode_jwt_payload(Base.id_token(tokens))
      user = Base.apply_profile_mapping(
        {
          id: profile["sub"],
          email: profile["email"],
          name: profile["name"],
          image: profile["picture"],
          emailVerified: !!profile["email_verified"]
        },
        profile,
        normalized
      )
      {
        user: user,
        data: profile
      }
    end,
    refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
      Base.refresh_access_token("https://oauth2.googleapis.com/token", refresh_token, client_id: primary_client_id, client_secret: client_secret)
    end
  }
end

.huggingface(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/better_auth/social_providers/huggingface.rb', line 7

def huggingface(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options)
  Base.oauth_provider(
    id: "huggingface",
    name: "Hugging Face",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://huggingface.co/oauth/authorize",
    token_endpoint: "https://huggingface.co/oauth/token",
    user_info_endpoint: "https://huggingface.co/oauth/userinfo",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["sub"],
        name: profile["name"] || profile["preferred_username"] || "",
        email: profile["email"],
        image: profile["picture"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
end

.kakao(client_id:, client_secret:, scopes: ["account_email", "profile_image", "profile_nickname"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/better_auth/social_providers/kakao.rb', line 7

def kakao(client_id:, client_secret:, scopes: ["account_email", "profile_image", "profile_nickname"], **options)
  Base.oauth_provider(
    id: "kakao",
    name: "Kakao",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://kauth.kakao.com/oauth/authorize",
    token_endpoint: "https://kauth.kakao.com/oauth/token",
    user_info_endpoint: "https://kapi.kakao.com/v2/user/me",
    scopes: scopes,
    profile_map: ->(profile) {
       = profile["kakao_account"] || {}
      kakao_profile = ["profile"] || {}
      {
        id: profile["id"].to_s,
        name: kakao_profile["nickname"] || ["name"] || "",
        email: ["email"],
        image: kakao_profile["profile_image_url"] || kakao_profile["thumbnail_image_url"],
        emailVerified: !!["is_email_valid"] && !!["is_email_verified"]
      }
    },
    **options
  )
end

.kick(client_id:, client_secret:, scopes: ["user:read"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/better_auth/social_providers/kick.rb', line 7

def kick(client_id:, client_secret:, scopes: ["user:read"], **options)
  Base.oauth_provider(
    id: "kick",
    name: "Kick",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://id.kick.com/oauth/authorize",
    token_endpoint: "https://id.kick.com/oauth/token",
    user_info_endpoint: "https://api.kick.com/public/v1/users",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      user = Array(profile["data"]).first || profile
      {
        id: user["user_id"],
        name: user["name"],
        email: user["email"],
        image: user["profile_picture"],
        emailVerified: false
      }
    },
    **options
  )
end

.line(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/better_auth/social_providers/line.rb', line 7

def line(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options)
  provider = Base.oauth_provider(
    id: "line",
    name: "LINE",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://access.line.me/oauth2/v2.1/authorize",
    token_endpoint: "https://api.line.me/oauth2/v2.1/token",
    user_info_endpoint: "https://api.line.me/oauth2/v2.1/userinfo",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["sub"] || profile["userId"],
        name: profile["name"] || profile["displayName"] || "",
        email: profile["email"],
        image: profile["picture"] || profile["pictureUrl"],
        emailVerified: false
      }
    },
    **options
  )
  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? }
  provider
end

.linear(client_id:, client_secret:, scopes: ["read"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/better_auth/social_providers/linear.rb', line 7

def linear(client_id:, client_secret:, scopes: ["read"], **options)
  provider = Base.oauth_provider(
    id: "linear",
    name: "Linear",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://linear.app/oauth/authorize",
    token_endpoint: "https://api.linear.app/oauth/token",
    scopes: scopes,
    profile_map: ->(profile) {
      viewer = profile.dig("data", "viewer") || {}
      {
        id: viewer["id"],
        name: viewer["name"],
        email: viewer["email"],
        image: viewer["avatarUrl"],
        emailVerified: false
      }
    },
    **options
  )
  provider[:get_user_info] = lambda do |tokens|
    custom = Base.option(provider[:options], :get_user_info)
    profile = custom ? custom.call(tokens) : Base.post_json(
      "https://api.linear.app/graphql",
      {query: "{ viewer { id name email avatarUrl active createdAt updatedAt } }"},
      "Authorization" => "Bearer #{Base.access_token(tokens)}"
    )
    return profile if Base.provider_user_info?(profile)

    mapped = provider[:options][:map_profile_to_user]&.call(profile) || {}
    viewer = profile.dig("data", "viewer") || {}
    {user: {id: viewer["id"], name: viewer["name"], email: viewer["email"], image: viewer["avatarUrl"], emailVerified: false}.merge(mapped), data: profile}
  end
  provider
end

.linkedin(client_id:, client_secret:, scopes: ["profile", "email", "openid"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/better_auth/social_providers/linkedin.rb', line 7

def linkedin(client_id:, client_secret:, scopes: ["profile", "email", "openid"], **options)
  Base.oauth_provider(
    id: "linkedin",
    name: "Linkedin",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://www.linkedin.com/oauth/v2/authorization",
    token_endpoint: "https://www.linkedin.com/oauth/v2/accessToken",
    user_info_endpoint: "https://api.linkedin.com/v2/userinfo",
    scopes: scopes,
    profile_map: ->(profile) {
      {
        id: profile["sub"],
        name: profile["name"],
        email: profile["email"],
        image: profile["picture"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
end

.microsoft(client_id:, client_secret: nil, tenant_id: "common", scopes: ["openid", "profile", "email", "User.Read", "offline_access"], **options) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/better_auth/social_providers/microsoft_entra_id.rb', line 20

def microsoft(client_id:, client_secret: nil, tenant_id: "common", scopes: ["openid", "profile", "email", "User.Read", "offline_access"], **options)
  normalized = Base.normalize_options(options)
  microsoft_provider(
    provider_id: "microsoft",
    provider_name: "Microsoft EntraID",
    client_id: client_id,
    client_secret: client_secret,
    tenant_id: normalized[:tenant_id] || tenant_id,
    scopes: scopes,
    **options
  )
end

.microsoft_email_verified?(profile, email) ⇒ Boolean

Returns:

  • (Boolean)


130
131
132
133
134
135
# File 'lib/better_auth/social_providers/microsoft_entra_id.rb', line 130

def microsoft_email_verified?(profile, email)
  return !!profile["email_verified"] if profile.key?("email_verified")

  Array(profile["verified_primary_email"]).include?(email) ||
    Array(profile["verified_secondary_email"]).include?(email)
end

.microsoft_entra_id(client_id:, client_secret:, tenant_id: "common", scopes: ["openid", "profile", "email", "User.Read", "offline_access"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
# File 'lib/better_auth/social_providers/microsoft_entra_id.rb', line 7

def microsoft_entra_id(client_id:, client_secret:, tenant_id: "common", scopes: ["openid", "profile", "email", "User.Read", "offline_access"], **options)
  normalized = Base.normalize_options(options)
  microsoft_provider(
    provider_id: "microsoft-entra-id",
    provider_name: "Microsoft Entra ID",
    client_id: client_id,
    client_secret: client_secret,
    tenant_id: normalized[:tenant_id] || tenant_id,
    scopes: scopes,
    **options
  )
end

.microsoft_provider(provider_id:, provider_name:, client_id:, client_secret:, tenant_id:, scopes:, **options) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/better_auth/social_providers/microsoft_entra_id.rb', line 33

def microsoft_provider(provider_id:, provider_name:, client_id:, client_secret:, tenant_id:, scopes:, **options)
  authority = options[:authority] || "https://login.microsoftonline.com"
  base = "#{authority.to_s.sub(%r{/+\z}, "")}/#{tenant_id}/oauth2/v2.0"
  normalized = Base.normalize_options(options)
  primary_client_id = Base.primary_client_id(client_id)
  {
    id: provider_id,
    name: provider_name,
    client_id: client_id,
    client_secret: client_secret,
    create_authorization_url: lambda do |data|
      verifier = data[:code_verifier] || data[:codeVerifier]
      Base.authorization_url(options[:authorization_endpoint] || "#{base}/authorize", {
        client_id: primary_client_id,
        redirect_uri: data[:redirect_uri] || data[:redirectURI],
        response_type: "code",
        scope: Base.selected_scopes(scopes, normalized, data),
        state: data[:state],
        code_challenge: verifier && Base.pkce_challenge(verifier),
        code_challenge_method: verifier && "S256",
        login_hint: data[:loginHint] || data[:login_hint],
        prompt: options[:prompt]
      })
    end,
    validate_authorization_code: lambda do |data|
      Base.post_form("#{base}/token", {
        client_id: primary_client_id,
        client_secret: client_secret,
        code: data[:code],
        code_verifier: data[:code_verifier] || data[:codeVerifier],
        grant_type: "authorization_code",
        redirect_uri: data[:redirect_uri] || data[:redirectURI]
      })
    end,
    verify_id_token: normalized[:verify_id_token] || lambda do |token, nonce = nil|
      return false if normalized[:disable_id_token_sign_in]

      issuers = nil
      unless %w[common organizations consumers].include?(tenant_id.to_s)
        issuers = "#{authority.to_s.sub(%r{/+\z}, "")}/#{tenant_id}/v2.0"
      end
      profile = Base.verify_jwt_with_jwks(
        token,
        jwks: normalized[:jwks],
        jwks_endpoint: normalized[:jwks_endpoint] || "#{authority.to_s.sub(%r{/+\z}, "")}/#{tenant_id}/discovery/v2.0/keys",
        algorithms: ["RS256"],
        issuers: issuers,
        audience: Array(client_id),
        nonce: nonce
      )

      !!(profile&.fetch("sub", nil) || profile&.fetch("oid", nil))
    end,
    get_user_info: lambda do |tokens|
      custom = normalized[:get_user_info]
      next custom.call(tokens) if custom

      profile = Base.id_token(tokens) ? Base.decode_jwt_payload(Base.id_token(tokens)) : {}
      profile = Base.get_json("https://graph.microsoft.com/v1.0/me", "Authorization" => "Bearer #{Base.access_token(tokens)}") if profile.empty?
      unless normalized[:disable_profile_photo]
        photo_size = normalized[:profile_photo_size] || 48
        photo = Base.get_bytes(
          "https://graph.microsoft.com/v1.0/me/photos/#{photo_size}x#{photo_size}/$value",
          "Authorization" => "Bearer #{Base.access_token(tokens)}"
        )
        profile["picture"] = "data:image/jpeg;base64, #{Base64.strict_encode64(photo)}" if photo
      end
      email = profile["email"] || profile["mail"] || profile["userPrincipalName"] || profile["preferred_username"]

      user = Base.apply_profile_mapping(
        {
          id: profile["sub"] || profile["id"] || profile["oid"],
          email: email,
          name: profile["name"] || profile["displayName"],
          image: profile["picture"],
          emailVerified: microsoft_email_verified?(profile, email)
        },
        profile,
        normalized
      )
      {
        user: user,
        data: profile
      }
    end,
    refresh_access_token: options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
      Base.refresh_access_token(
        "#{base}/token",
        refresh_token,
        client_id: primary_client_id,
        client_secret: client_secret,
        extra_params: {scope: Base.selected_scopes(scopes, normalized, {}).join(" ")}
      )
    end
  }
end


7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/better_auth/social_providers/naver.rb', line 7

def naver(client_id:, client_secret:, scopes: ["profile", "email"], **options)
  Base.oauth_provider(
    id: "naver",
    name: "Naver",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://nid.naver.com/oauth2.0/authorize",
    token_endpoint: "https://nid.naver.com/oauth2.0/token",
    user_info_endpoint: "https://openapi.naver.com/v1/nid/me",
    scopes: scopes,
    profile_map: ->(profile) {
      data = profile["response"] || {}
      {
        id: data["id"],
        name: data["name"] || data["nickname"] || "",
        email: data["email"],
        image: data["profile_image"],
        emailVerified: false
      }
    },
    **options
  )
end

.notion(client_id:, client_secret:, scopes: [], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/better_auth/social_providers/notion.rb', line 7

def notion(client_id:, client_secret:, scopes: [], **options)
  Base.oauth_provider(
    id: "notion",
    name: "Notion",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://api.notion.com/v1/oauth/authorize",
    token_endpoint: "https://api.notion.com/v1/oauth/token",
    user_info_endpoint: "https://api.notion.com/v1/users/me",
    scopes: scopes,
    auth_params: {owner: "user"},
    user_info_headers: {"Notion-Version" => "2022-06-28"},
    profile_map: ->(profile) {
      user = profile.dig("bot", "owner", "user") || profile
      {
        id: user["id"],
        name: user["name"] || "",
        email: user.dig("person", "email"),
        image: user["avatar_url"],
        emailVerified: false
      }
    },
    **options
  )
end

.paybin(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/better_auth/social_providers/paybin.rb', line 7

def paybin(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
  issuer = (options[:issuer] || "https://idp.paybin.io").to_s.sub(%r{/+\z}, "")
  Base.oauth_provider(
    id: "paybin",
    name: "Paybin",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "#{issuer}/oauth2/authorize",
    token_endpoint: "#{issuer}/oauth2/token",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["sub"],
        name: profile["name"] || profile["preferred_username"] || "",
        email: profile["email"],
        image: profile["picture"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
end

.paypal(client_id:, client_secret:, scopes: [], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/better_auth/social_providers/paypal.rb', line 7

def paypal(client_id:, client_secret:, scopes: [], **options)
  sandbox = (options[:environment] || "sandbox").to_s == "sandbox"
  auth_host = sandbox ? "https://www.sandbox.paypal.com" : "https://www.paypal.com"
  api_host = sandbox ? "https://api-m.sandbox.paypal.com" : "https://api-m.paypal.com"
  provider = Base.oauth_provider(
    id: "paypal",
    name: "PayPal",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "#{auth_host}/signin/authorize",
    token_endpoint: "#{api_host}/v1/oauth2/token",
    user_info_endpoint: "#{api_host}/v1/identity/oauth2/userinfo?schema=paypalv1.1",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["user_id"],
        name: profile["name"],
        email: profile["email"],
        image: profile["picture"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
  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"] }
  provider
end

.polar(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/better_auth/social_providers/polar.rb', line 7

def polar(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options)
  Base.oauth_provider(
    id: "polar",
    name: "Polar",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://polar.sh/oauth2/authorize",
    token_endpoint: "https://api.polar.sh/v1/oauth2/token",
    user_info_endpoint: "https://api.polar.sh/v1/oauth2/userinfo",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["id"],
        name: profile["public_name"] || profile["username"] || "",
        email: profile["email"],
        image: profile["avatar_url"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
end

.railway(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/better_auth/social_providers/railway.rb', line 7

def railway(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
  primary_client_id = Base.primary_client_id(client_id)
  credentials = Base64.strict_encode64("#{primary_client_id}:#{client_secret}")
  token_endpoint = options[:token_endpoint] || options[:tokenEndpoint] || "https://backboard.railway.com/oauth/token"
  provider = Base.oauth_provider(
    id: "railway",
    name: "Railway",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://backboard.railway.com/oauth/auth",
    token_endpoint: "https://backboard.railway.com/oauth/token",
    user_info_endpoint: "https://backboard.railway.com/oauth/me",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["sub"],
        name: profile["name"],
        email: profile["email"],
        image: profile["picture"],
        emailVerified: false
      }
    },
    **options
  )
  provider[:validate_authorization_code] = lambda do |data|
    Base.post_form_json(token_endpoint, {
      code: data[:code],
      code_verifier: data[:code_verifier] || data[:codeVerifier],
      grant_type: "authorization_code",
      redirect_uri: options[:redirect_uri] || options[:redirectURI] || data[:redirect_uri] || data[:redirectURI]
    }, {"Authorization" => "Basic #{credentials}"})
  end
  provider[:refresh_access_token] = options[:refresh_access_token] || options[:refreshAccessToken] || lambda do |refresh_token|
    Base.normalize_tokens(Base.post_form_json(token_endpoint, {
      grant_type: "refresh_token",
      refresh_token: refresh_token
    }, {"Authorization" => "Basic #{credentials}"}))
  end
  provider
end

.reddit(client_id:, client_secret:, scopes: ["identity"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/better_auth/social_providers/reddit.rb', line 7

def reddit(client_id:, client_secret:, scopes: ["identity"], **options)
  Base.oauth_provider(
    id: "reddit",
    name: "Reddit",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://www.reddit.com/api/v1/authorize",
    token_endpoint: "https://www.reddit.com/api/v1/access_token",
    user_info_endpoint: "https://oauth.reddit.com/api/v1/me",
    user_info_headers: {"User-Agent" => "better-auth"},
    scopes: scopes,
    auth_params: ->(_data, opts) { {duration: opts[:duration]} },
    profile_map: ->(profile) {
      {
        id: profile["id"],
        name: profile["name"],
        email: profile["oauth_client_id"],
        image: profile["icon_img"].to_s.split("?").first,
        emailVerified: !!profile["has_verified_email"]
      }
    },
    **options
  )
end

.roblox(client_id:, client_secret:, scopes: ["openid", "profile"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/better_auth/social_providers/roblox.rb', line 7

def roblox(client_id:, client_secret:, scopes: ["openid", "profile"], **options)
  Base.oauth_provider(
    id: "roblox",
    name: "Roblox",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://apis.roblox.com/oauth/v1/authorize",
    token_endpoint: "https://apis.roblox.com/oauth/v1/token",
    user_info_endpoint: "https://apis.roblox.com/oauth/v1/userinfo",
    scopes: scopes,
    auth_params: ->(_data, opts) { {prompt: opts[:prompt] || "select_account consent"} },
    profile_map: ->(profile) {
      {
        id: profile["sub"],
        name: profile["nickname"] || profile["preferred_username"] || "",
        email: profile["preferred_username"],
        image: profile["picture"],
        emailVerified: false
      }
    },
    **options
  )
end

.salesforce(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/better_auth/social_providers/salesforce.rb', line 7

def salesforce(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
  host = if options[:loginUrl] || options[:login_url]
    "https://#{options[:loginUrl] || options[:login_url]}"
  elsif options[:environment].to_s == "sandbox"
    "https://test.salesforce.com"
  else
    "https://login.salesforce.com"
  end
  Base.oauth_provider(
    id: "salesforce",
    name: "Salesforce",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "#{host}/services/oauth2/authorize",
    token_endpoint: "#{host}/services/oauth2/token",
    user_info_endpoint: "#{host}/services/oauth2/userinfo",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["user_id"],
        name: profile["name"],
        email: profile["email"],
        image: profile.dig("photos", "picture") || profile.dig("photos", "thumbnail"),
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
end

.slack(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/better_auth/social_providers/slack.rb', line 7

def slack(client_id:, client_secret:, scopes: ["openid", "profile", "email"], **options)
  Base.oauth_provider(
    id: "slack",
    name: "Slack",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://slack.com/openid/connect/authorize",
    token_endpoint: "https://slack.com/api/openid.connect.token",
    user_info_endpoint: "https://slack.com/api/openid.connect.userInfo",
    scopes: scopes,
    profile_map: ->(profile) {
      {
        id: profile["https://slack.com/user_id"] || profile["sub"],
        name: profile["name"] || "",
        email: profile["email"],
        image: profile["picture"] || profile["https://slack.com/user_image_512"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
end

.spotify(client_id:, client_secret:, scopes: ["user-read-email"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/better_auth/social_providers/spotify.rb', line 7

def spotify(client_id:, client_secret:, scopes: ["user-read-email"], **options)
  Base.oauth_provider(
    id: "spotify",
    name: "Spotify",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://accounts.spotify.com/authorize",
    token_endpoint: "https://accounts.spotify.com/api/token",
    user_info_endpoint: "https://api.spotify.com/v1/me",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["id"],
        name: profile["display_name"],
        email: profile["email"],
        image: Array(profile["images"]).first&.fetch("url", nil),
        emailVerified: false
      }
    },
    **options
  )
end

.tiktok(client_id:, client_secret:, scopes: ["user.info.profile"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/better_auth/social_providers/tiktok.rb', line 7

def tiktok(client_id:, client_secret:, scopes: ["user.info.profile"], **options)
  client_key = options[:client_key] || options[:clientKey] || client_id
  Base.oauth_provider(
    id: "tiktok",
    name: "TikTok",
    client_id: client_key,
    client_secret: client_secret,
    authorization_endpoint: "https://www.tiktok.com/v2/auth/authorize",
    token_endpoint: "https://open.tiktokapis.com/v2/oauth/token/",
    user_info_endpoint: "https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_large_url,display_name,username",
    scopes: scopes,
    scope_separator: ",",
    auth_params: {client_key: client_key},
    token_params: {client_key: client_key},
    profile_map: ->(profile) {
      user = profile.dig("data", "user") || profile
      {
        id: user["open_id"],
        name: user["display_name"] || user["username"] || "",
        email: user["email"] || user["username"],
        image: user["avatar_large_url"],
        emailVerified: false
      }
    },
    **options
  )
end

.twitch(client_id:, client_secret:, scopes: ["user:read:email", "openid"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/better_auth/social_providers/twitch.rb', line 7

def twitch(client_id:, client_secret:, scopes: ["user:read:email", "openid"], **options)
  Base.oauth_provider(
    id: "twitch",
    name: "Twitch",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://id.twitch.tv/oauth2/authorize",
    token_endpoint: "https://id.twitch.tv/oauth2/token",
    scopes: scopes,
    auth_params: {
      claims: JSON.generate({
        userinfo: {
          email: nil,
          email_verified: nil,
          preferred_username: nil,
          picture: nil
        }
      })
    },
    profile_map: ->(profile) {
      {
        id: profile["sub"],
        name: profile["preferred_username"],
        email: profile["email"],
        image: profile["picture"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
end

.twitter(client_id:, client_secret:, scopes: ["users.read", "tweet.read", "offline.access", "users.email"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/better_auth/social_providers/twitter.rb', line 7

def twitter(client_id:, client_secret:, scopes: ["users.read", "tweet.read", "offline.access", "users.email"], **options)
  Base.oauth_provider(
    id: "twitter",
    name: "Twitter",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://x.com/i/oauth2/authorize",
    token_endpoint: "https://api.x.com/2/oauth2/token",
    user_info_endpoint: "https://api.x.com/2/users/me?user.fields=profile_image_url,verified",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      data = profile["data"] || profile
      {
        id: data["id"],
        name: data["name"],
        email: data["email"] || data["username"],
        image: data["profile_image_url"],
        emailVerified: !!data["confirmed_email"]
      }
    },
    **options
  )
end

.vercel(client_id:, client_secret:, scopes: [], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/better_auth/social_providers/vercel.rb', line 7

def vercel(client_id:, client_secret:, scopes: [], **options)
  provider = Base.oauth_provider(
    id: "vercel",
    name: "Vercel",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://vercel.com/oauth/authorize",
    token_endpoint: "https://api.vercel.com/login/oauth/token",
    user_info_endpoint: "https://api.vercel.com/login/oauth/userinfo",
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      {
        id: profile["sub"],
        name: profile["name"] || profile["preferred_username"] || "",
        email: profile["email"],
        image: profile["picture"],
        emailVerified: !!profile["email_verified"]
      }
    },
    **options
  )
  provider[:create_authorization_url] = lambda do |data|
    verifier = data[:code_verifier] || data[:codeVerifier]
    raise Error, "codeVerifier is required for Vercel" if verifier.to_s.empty?

    selected_scopes = Base.selected_scopes(scopes, Base.normalize_options(options), data)
    Base.authorization_url(options[:authorization_endpoint] || "https://vercel.com/oauth/authorize", {
      client_id: Base.primary_client_id(client_id),
      redirect_uri: options[:redirect_uri] || options[:redirectURI] || data[:redirect_uri] || data[:redirectURI],
      response_type: "code",
      scope: selected_scopes.empty? ? nil : selected_scopes,
      state: data[:state],
      code_challenge: Base.pkce_challenge(verifier),
      code_challenge_method: "S256"
    })
  end
  provider
end

.vk(client_id:, client_secret:, scopes: ["email", "phone"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/better_auth/social_providers/vk.rb', line 7

def vk(client_id:, client_secret:, scopes: ["email", "phone"], **options)
  Base.oauth_provider(
    id: "vk",
    name: "VK",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://id.vk.com/authorize",
    token_endpoint: "https://id.vk.com/oauth2/auth",
    user_info_endpoint: "https://id.vk.com/oauth2/user_info",
    user_info_method: :post,
    user_info_body: {client_id: client_id},
    scopes: scopes,
    pkce: true,
    profile_map: ->(profile) {
      user = profile["user"] || profile
      {
        id: user["user_id"],
        name: [user["first_name"], user["last_name"]].compact.join(" "),
        email: user["email"],
        image: user["avatar"],
        emailVerified: false
      }
    },
    **options
  )
end

.wechat(client_id:, client_secret:, scopes: ["snsapi_login"], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/better_auth/social_providers/wechat.rb', line 7

def wechat(client_id:, client_secret:, scopes: ["snsapi_login"], **options)
  normalized = Base.normalize_options(options)
  provider = Base.oauth_provider(
    id: "wechat",
    name: "WeChat",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://open.weixin.qq.com/connect/qrconnect",
    token_endpoint: "https://api.weixin.qq.com/sns/oauth2/access_token",
    user_info_endpoint: "https://api.weixin.qq.com/sns/userinfo",
    scopes: scopes,
    scope_separator: ",",
    profile_map: ->(profile) {
      {
        id: profile["unionid"] || profile["openid"],
        name: profile["nickname"],
        email: profile["email"],
        image: profile["headimgurl"],
        emailVerified: false
      }
    },
    **options
  )
  provider[:create_authorization_url] = lambda do |data|
    "#{Base.authorization_url("https://open.weixin.qq.com/connect/qrconnect", {
      appid: client_id,
      redirect_uri: normalized[:redirect_uri] || data[:redirect_uri] || data[:redirectURI],
      response_type: "code",
      scope: Base.selected_scopes(scopes, normalized, data).join(","),
      state: data[:state],
      lang: options[:lang] || "cn"
    })}#wechat_redirect"
  end
  provider[:validate_authorization_code] = lambda do |data|
    url = Base.authorization_url("https://api.weixin.qq.com/sns/oauth2/access_token", {
      appid: client_id,
      secret: client_secret,
      code: data[:code],
      grant_type: "authorization_code"
    })
    payload = Base.get_json(url)
    if !payload || payload["errcode"]
      raise Error, "Failed to validate authorization code: #{payload&.fetch("errmsg", nil) || "Unknown error"}"
    end

    Base.normalize_tokens(payload).merge(
      "openid" => payload["openid"],
      "unionid" => payload["unionid"]
    ).compact
  end
  provider[:refresh_access_token] = normalized[:refresh_access_token] || lambda do |refresh_token|
    url = Base.authorization_url("https://api.weixin.qq.com/sns/oauth2/refresh_token", {
      appid: client_id,
      grant_type: "refresh_token",
      refresh_token: refresh_token
    })
    payload = Base.get_json(url)
    if !payload || payload["errcode"]
      raise Error, "Failed to refresh access token: #{payload&.fetch("errmsg", nil) || "Unknown error"}"
    end

    Base.normalize_tokens(payload).merge(
      "openid" => payload["openid"],
      "unionid" => payload["unionid"]
    ).compact
  end
  provider[:get_user_info] = lambda do |tokens|
    custom = normalized[:get_user_info]
    next custom.call(tokens) if custom

    openid = tokens["openid"] || tokens[:openid]
    next nil if openid.to_s.empty?

    url = Base.authorization_url("https://api.weixin.qq.com/sns/userinfo", {
      access_token: Base.access_token(tokens),
      openid: openid,
      lang: "zh_CN"
    })
    profile = Base.get_json(url)
    next nil if !profile || profile["errcode"]

    user = Base.apply_profile_mapping(
      {
        id: profile["unionid"] || profile["openid"] || openid,
        name: profile["nickname"],
        email: profile["email"],
        image: profile["headimgurl"],
        emailVerified: false
      },
      profile,
      normalized
    )
    {user: user, data: profile}
  end
  provider
end

.zoom(client_id:, client_secret:, scopes: [], **options) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/better_auth/social_providers/zoom.rb', line 7

def zoom(client_id:, client_secret:, scopes: [], **options)
  Base.oauth_provider(
    id: "zoom",
    name: "Zoom",
    client_id: client_id,
    client_secret: client_secret,
    authorization_endpoint: "https://zoom.us/oauth/authorize",
    token_endpoint: "https://zoom.us/oauth/token",
    user_info_endpoint: "https://api.zoom.us/v2/users/me",
    scopes: scopes,
    pkce: options.fetch(:pkce, true),
    profile_map: ->(profile) {
      {
        id: profile["id"],
        name: profile["display_name"],
        email: profile["email"],
        image: profile["pic_url"],
        emailVerified: !!profile["verified"]
      }
    },
    **options
  )
end