FEATURE: Login with Discord (#8053)

FEATURE: Login with Discord (#8053)

This migrates the functionality of discourse-plugin-discord-auth into core.

The plugin will automatically disable itself when core is updated: Automatically disable plugin when core is updated with Discord support · discourse/discourse-plugin-discord-auth@fd08678 · GitHub

For setup instructions, visit https://meta.discourse.org/t/configuring-discord-login-for-discourse/127129

diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss
index f72a14d..de22978 100644
--- a/app/assets/stylesheets/common/components/buttons.scss
+++ b/app/assets/stylesheets/common/components/buttons.scss
@@ -231,6 +231,12 @@
       background: lighten($github, 20%);
     }
   }
+  &.discord {
+    background: $discord;
+    &:hover {
+      background: darken($discord, 10%);
+    }
+  }
 }
 
 // Button Sizes
diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss
index 644d3ee..0895022 100644
--- a/app/assets/stylesheets/common/foundation/variables.scss
+++ b/app/assets/stylesheets/common/foundation/variables.scss
@@ -23,6 +23,7 @@ $facebook: #4267b2 !default;
 $cas: #70ba61 !default;
 $twitter: #1da1f2 !default;
 $github: #100e0f !default;
+$discord: #7289da !default;
 
 // Badge color variables
 // --------------------------------------------------
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index c2dbb4c..e5b2950 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1469,6 +1469,10 @@ en:
         name: "GitHub"
         title: "with GitHub"
         message: "Authenticating with GitHub (make sure pop up blockers are not enabled)"
+      discord:
+        name: "Discord"
+        title: "with Discord"
+        message: "Authenticating with Discord"
     invites:
       accept_title: "Invitation"
       welcome_to: "Welcome to %{site_name}!"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 7030ded..caf705c 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1551,6 +1551,11 @@ en:
     github_client_id: "Client id for Github authentication, registered at <a href='https://github.com/settings/developers/' target='_blank'>https://github.com/settings/developers</a>"
     github_client_secret: "Client secret for Github authentication, registered at <a href='https://github.com/settings/developers/' target='_blank'>https://github.com/settings/developers</a>"
 
+    enable_discord_logins: 'Allow users to authenticate using Discord?'
+    discord_client_id: 'Discord Client ID (need one? visit <a href="https://discordapp.com/developers/applications/me">the Discord developer portal</a>)'
+    discord_secret: 'Discord Secret Key'
+    discord_trusted_guilds: 'Only allow members of these Discord guilds to login via Discord. Use the numeric ID for the guild. For more information, check the instructions <a href="https://meta.discourse.org/t/configuring-discord-login-for-discourse/127129">here</a>. Leave blank to allow any guild.'
+
     readonly_mode_during_backup: "Enable read only mode while taking a backup"
     enable_backups: "Allow administrators to create backups of the forum"
     allow_restore: "Allow restore, which can replace ALL site data! Leave false unless you plan to restore a backup"
@@ -4569,3 +4574,6 @@ en:
 
   email_style:
     html_missing_placeholder: "The html template must include %{placeholder}"
+
+  discord:
+    not_in_allowed_guild: 'Authentication failed. You are not a member of a permitted Discord guild.'
\ No newline at end of file
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 1129cc0..67683bc 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -390,6 +390,16 @@ login:
     default: ""
     regex: "^[a-f0-9]+$"
     secret: true
+  enable_discord_logins:
+    default: false
+  discord_client_id:
+    default: ''
+  discord_secret:
+    default: ''
+    secret: true
+  discord_trusted_guilds:
+    default: ''
+    type: list
   enable_sso:
     client: true
     default: false
diff --git a/lib/auth.rb b/lib/auth.rb
index 2c4594d..74e924b 100644
--- a/lib/auth.rb
+++ b/lib/auth.rb
@@ -12,3 +12,4 @@ require_dependency 'auth/github_authenticator'
 require_dependency 'auth/twitter_authenticator'
 require_dependency 'auth/google_oauth2_authenticator'
 require_dependency 'auth/instagram_authenticator'
+require_dependency 'auth/discord_authenticator'
diff --git a/lib/auth/discord_authenticator.rb b/lib/auth/discord_authenticator.rb
new file mode 100644
index 0000000..0d54f24
--- /dev/null
+++ b/lib/auth/discord_authenticator.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator
+  class DiscordStrategy < OmniAuth::Strategies::OAuth2
+    option :name, 'discord'
+    option :scope, 'identify email guilds'
+
+    option :client_options,
+            site: 'https://discordapp.com/api',
+            authorize_url: 'oauth2/authorize',
+            token_url: 'oauth2/token'
+
+    option :authorize_options, %i[scope permissions]
+
+    uid { raw_info['id'] }
+
+    info do
+      {
+        name: raw_info['username'],
+        email: raw_info['verified'] ? raw_info['email'] : nil,
+        image: "https://cdn.discordapp.com/avatars/#{raw_info['id']}/#{raw_info['avatar']}"
+      }
+    end
+
+    extra do
+      {
+        'raw_info' => raw_info
+      }
+    end
+
+    def raw_info
+      @raw_info ||= access_token.get('users/@me').parsed.
+        merge(guilds: access_token.get('users/@me/guilds').parsed)
+    end
+
+    def callback_url
+      full_host + script_name + callback_path
+    end
+  end
+
+  def name
+    'discord'
+  end
+
+  def enabled?
+    SiteSetting.enable_discord_logins?
+  end
+
+  def register_middleware(omniauth)
+    omniauth.provider DiscordStrategy,
+                      setup: lambda { |env|
+                        strategy = env["omniauth.strategy"]
+                        strategy.options[:client_id] = SiteSetting.discord_client_id
+                        strategy.options[:client_secret] = SiteSetting.discord_secret
+                      }
+    end
+
+  def after_authenticate(auth_token, existing_account: nil)
+    allowed_guild_ids = SiteSetting.discord_trusted_guilds.split("|")
+
+    if allowed_guild_ids.length > 0
+      user_guild_ids = auth_token.extra[:raw_info][:guilds].map { |g| g['id'] }
+      if (user_guild_ids & allowed_guild_ids).empty? # User is not in any allowed guilds
+        return Auth::Result.new.tap do |auth_result|
+          auth_result.failed = true
+          auth_result.failed_reason = I18n.t("discord.not_in_allowed_guild")
+        end
+      end
+    end
+
+    super
+  end
+end
diff --git a/lib/discourse.rb b/lib/discourse.rb
index 5a13c19..d44f028 100644
--- a/lib/discourse.rb
+++ b/lib/discourse.rb
@@ -254,7 +254,8 @@ module Discourse
     Auth::AuthProvider.new(authenticator: Auth::GoogleOAuth2Authenticator.new, frame_width: 850, frame_height: 500), # Custom icon implemented in client
     Auth::AuthProvider.new(authenticator: Auth::GithubAuthenticator.new, icon: "fab-github"),
     Auth::AuthProvider.new(authenticator: Auth::TwitterAuthenticator.new, icon: "fab-twitter"),
-    Auth::AuthProvider.new(authenticator: Auth::InstagramAuthenticator.new, icon: "fab-instagram")
+    Auth::AuthProvider.new(authenticator: Auth::InstagramAuthenticator.new, icon: "fab-instagram"),
+    Auth::AuthProvider.new(authenticator: Auth::DiscordAuthenticator.new, icon: "fab-discord", full_screen_login: true)
   ]
 
   def self.auth_providers
diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb
index 413c0ce..6bdc6d2 100644
--- a/lib/svg_sprite/svg_sprite.rb
+++ b/lib/svg_sprite/svg_sprite.rb
@@ -71,6 +71,7 @@ module SvgSprite
     "fab-android",
     "fab-apple",
     "fab-chrome",
+    "fab-discord",
     "fab-discourse",
     "fab-facebook-square",
     "fab-facebook",
diff --git a/spec/components/auth/discord_authenticator_spec.rb b/spec/components/auth/discord_authenticator_spec.rb
new file mode 100644
index 0000000..3a9ad73
--- /dev/null
+++ b/spec/components/auth/discord_authenticator_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Auth::DiscordAuthenticator do
+  let(:hash) {

[... diff too long, it was truncated ...]

GitHub sha: be96c447

This isn’t a huge concern but if you only want to specify a default value and no other parameters it is cleaner to say:

enable_discourse_logins: false etc

1 Like