Module: BetterAuth::SocialProviders

Defined in:
lib/better_auth/social_providers/base.rb,
lib/better_auth/social_providers/apple.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/discord.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
# File 'lib/better_auth/social_providers/apple.rb', line 7

def apple(client_id:, client_secret:, scopes: ["email", "name"], **options)
  {
    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: client_id,
        redirect_uri: data[:redirect_uri] || data[:redirectURI],
        response_type: "code id_token",
        response_mode: options[:response_mode] || options[:responseMode] || "form_post",
        scope: data[:scopes] || scopes,
        state: data[:state]
      })
    end,
    validate_authorization_code: lambda do |data|
      Base.post_form("https://appleid.apple.com/auth/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|
      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", "firstName")
      last_name = apple_user.dig(:name, :lastName) || apple_user.dig("name", "lastName")
      full_name = [name, last_name].compact.join(" ").strip
      full_name = profile["name"] || " " if full_name.empty?

      {
        user: {
          id: profile["sub"],
          email: profile["email"],
          name: full_name,
          image: profile["picture"],
          emailVerified: profile["email_verified"] == true || profile["email_verified"] == "true"
        },
        data: profile.merge("name" => full_name)
      }
    end
  }
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
# File 'lib/better_auth/social_providers/discord.rb', line 7

def discord(client_id:, client_secret:, scopes: ["identify", "email"], **options)
  {
    id: "discord",
    name: "Discord",
    client_id: client_id,
    client_secret: client_secret,
    create_authorization_url: lambda do |data|
      selected_scopes = data[:scopes] || scopes
      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|
      profile = Base.get_json("https://discord.com/api/users/@me", "Authorization" => "Bearer #{Base.access_token(tokens)}")
      {
        user: {
          id: profile["id"],
          email: profile["email"],
          name: profile["global_name"] || profile["username"] || "",
          image: discord_avatar_url(profile),
          emailVerified: !!profile["verified"]
        },
        data: profile
      }
    end
  }
end

.discord_avatar_url(profile) ⇒ Object



51
52
53
54
55
56
57
# File 'lib/better_auth/social_providers/discord.rb', line 51

def discord_avatar_url(profile)
  avatar = profile["avatar"]
  return nil unless avatar

  format = avatar.start_with?("a_") ? "gif" : "png"
  "https://cdn.discordapp.com/avatars/#{profile["id"]}/#{avatar}.#{format}"
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
# File 'lib/better_auth/social_providers/github.rb', line 7

def github(client_id:, client_secret:, scopes: ["read:user", "user:email"], **options)
  {
    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: data[:scopes] || scopes,
        state: data[:state],
        login_hint: data[:loginHint] || data[:login_hint],
        prompt: options[:prompt]
      })
    end,
    validate_authorization_code: lambda do |data|
      Base.post_form("https://github.com/login/oauth/access_token", {
        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|
      headers = {
        "Authorization" => "Bearer #{Base.access_token(tokens)}",
        "Accept" => "application/json",
        "User-Agent" => "better-auth"
      }
      profile = Base.get_json("https://api.github.com/user", headers)
      emails = Base.get_json("https://api.github.com/user/emails", headers)
      primary = Array(emails).find { |email| email["email"] == profile["email"] } ||
        Array(emails).find { |email| email["primary"] } ||
        Array(emails).first ||
        {}

      {
        user: {
          id: profile["id"].to_s,
          email: profile["email"] || primary["email"],
          name: profile["name"] || profile["login"],
          image: profile["avatar_url"],
          emailVerified: !!primary["verified"]
        },
        data: profile
      }
    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
# 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}, "")
  {
    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: data[:scopes] || scopes,
        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|
      profile = Base.get_json("#{base}/api/v4/user", "Authorization" => "Bearer #{Base.access_token(tokens)}")
      return nil if profile["state"] && profile["state"] != "active"

      {
        user: {
          id: profile["id"].to_s,
          email: profile["email"],
          name: profile["name"] || profile["username"],
          image: profile["avatar_url"],
          emailVerified: !!profile["email_verified"]
        },
        data: profile
      }
    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
# File 'lib/better_auth/social_providers/google.rb', line 7

def google(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
  {
    id: "google",
    name: "Google",
    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] || "https://accounts.google.com/o/oauth2/v2/auth", {
        client_id: client_id,
        redirect_uri: data[:redirect_uri] || data[:redirectURI],
        response_type: "code",
        scope: data[:scopes] || scopes,
        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: 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|
      profile = if Base.id_token(tokens)
        Base.decode_jwt_payload(Base.id_token(tokens))
      else
        Base.get_json(
          "https://openidconnect.googleapis.com/v1/userinfo",
          "Authorization" => "Bearer #{Base.access_token(tokens)}"
        )
      end

      {
        user: {
          id: profile["sub"],
          email: profile["email"],
          name: profile["name"],
          image: profile["picture"],
          emailVerified: !!profile["email_verified"]
        },
        data: profile
      }
    end
  }
end

.microsoft_email_verified?(profile, email) ⇒ Boolean

Returns:

  • (Boolean)


58
59
60
61
62
63
# File 'lib/better_auth/social_providers/microsoft_entra_id.rb', line 58

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
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
# 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)
  authority = options[:authority] || "https://login.microsoftonline.com"
  base = "#{authority.to_s.sub(%r{/+\z}, "")}/#{tenant_id}/oauth2/v2.0"
  {
    id: "microsoft-entra-id",
    name: "Microsoft Entra ID",
    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: client_id,
        redirect_uri: data[:redirect_uri] || data[:redirectURI],
        response_type: "code",
        scope: data[:scopes] || scopes,
        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: 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|
      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?
      email = profile["email"] || profile["mail"] || profile["userPrincipalName"] || profile["preferred_username"]

      {
        user: {
          id: profile["sub"] || profile["id"] || profile["oid"],
          email: email,
          name: profile["name"] || profile["displayName"],
          image: profile["picture"],
          emailVerified: microsoft_email_verified?(profile, email)
        },
        data: profile
      }
    end
  }
end