How to Implement Login with Discord feature in Rails 7 using Devise

sectasy0

sectasy

Posted on February 5, 2023

How to Implement Login with Discord feature in Rails 7 using Devise

Discord is a widely used communication platform among gamers and communities around the world. Integrating the Discord login feature into your Ruby on Rails application can provide a more streamlined and secure way for users to sign up and log in.

Step 1: Set up the Discord Application

To integrate with Discord OAuth, you first need to set up a Discord Application. To do this, follow these steps:

  1. Log in to the Discord Developer Portal (https://discord.com/developers/applications)
  2. Click on the New Application button
  3. Give your application a name and click on the Create button
  4. From the OAuth2 section, copy CLIENT ID and CLIENT SECRET
  5. Paste your redirection URL, typically it will be https://<your-domain>/user/auth/discord/callback

NOTE: For the integration to work correctly, the server must be running with secure HTTPS, which is best done on a VPS using a domain. If you don't have a VPS, you can get one for free from Oracle, and a free domain from https://free-for.dev/#/?id=domain.

Step 2: Add the required Gems to your Gemfile

gem 'devise', '~> 4.8.1'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-discord', '~> 1.0.0'
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure the omniauth-discord gem

Enter the saved data from Step 1 into your credentials.yml file

EDITOR=vim rails credentials:edit
Enter fullscreen mode Exit fullscreen mode
discord:
    client_id: 1068564434567456784
    secret: BCf6heHXdcCc5Lwy5YL5R5BJ6SDm6QK2
Enter fullscreen mode Exit fullscreen mode

Next, set the appropriate configuration for the Discord provider in the config/initializers/devise.rb file by adding the following two lines:

discord_oauth = Rails.application.credentials.discord
config.omniauth :discord, discord_oauth[:client_id], discord_oauth[:secret], scope: 'email identify'
Enter fullscreen mode Exit fullscreen mode

Step 4: Add a method that creates a user with omniauth to the model.

def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.provider = auth.provider
      user.uid = auth.uid
      user.username = auth.info.name
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20]
    end
end
Enter fullscreen mode Exit fullscreen mode

If you want to download user profile pictures from discord, it's best if you use the carrierwave gem

gem 'carrierwave', '~> 2.2.3'
gem 'carrierwave-i18n', '~> 0.2.0'
Enter fullscreen mode Exit fullscreen mode

And create a propier uploader app/uploaders/avatar_uploader.rb

class AvatarUploader < CarrierWave::Uploader::Base
    include CarrierWave::MiniMagick

    storage :file
    process :resize_to_fit => [ 150, 150 ]

    def store_dir
        "uploads/#{model.class.to_s.underscore}"
    end

    def size_range
        0..1.megabytes
    end

    def extension_allowlist
        %w(jpg jpeg png)
    end

    def filename
        "#{model.id}.#{file&.extension}" if file
    end
end
Enter fullscreen mode Exit fullscreen mode

Next, mount uploader to your model.

mount_uploader :avatar, AvatarUploader
Enter fullscreen mode Exit fullscreen mode

NOTE: Gem carrierwave uses the MiniMagick library to work with photos. Don't forget to install this library on your system (sudo apt-get install libmagickwand-dev).

After that, let's go back to our self.from_omniauth method for a moment to add support for downloading avatars.

def self.from_omniauth(auth)
    image_available = Faraday.get(auth.info.image).status == 200

    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.provider = auth.provider
      user.uid = auth.uid
      user.username = auth.info.name
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20]
      user.remote_image_url = auth.info.image if image_available
    end
 end
Enter fullscreen mode Exit fullscreen mode
image_available = Faraday.get(auth.info.image).status == 200
Enter fullscreen mode Exit fullscreen mode

if the user does not have an avatar on discord, then under auth.info.image we will get the value nil (similarly in the case if for some reason the avatar on cdn discord is unavailable) and our uploader will return an error, to prevent this you need to first check if the avatar is correct.

account.remote_image_url = auth.info.image if image_available
Enter fullscreen mode Exit fullscreen mode

The remote_image_url method comes from our uploader. This method is to download the avatar from the Discord CDN and save it locally on the user.

Step 5: Implement the Omniauth Callback Controller

To handle the login from the Discord provider, you need to implement a callback controller. To do this, create a new file in your application's app/controllers directory named omniauth_callbacks_controller.rb with the following code:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
    skip_before_action :authenticate_account!

    def discord
        @user = User.from_omniauth(request.env["omniauth.auth"])

        sign_in(:account, @user)

        # in my case response was incorrect, so I made it by myself.
        redirect_to after_sign_in_path_for(@user),
                  notice: t('devise.omniauth_callbacks.success', kind: @user.provider)
    end
end
Enter fullscreen mode Exit fullscreen mode

Step 6: Add a discord login button to view

Now that you have completed the setup and configuration, it's time to implement the "Login with Discord" button in your application. You can do this by adding the following code to your view file:

= button_to("Log in with Discord", user_discord_omniauth_authorize_path)
Enter fullscreen mode Exit fullscreen mode

Step 7: Test the integration

Finally, test the integration by running your Rails application and clicking on the "Login with Discord" button. If everything has been set up correctly, you should be redirected to Discord for authentication and then back to your application where you will be logged in as the corresponding user.

And you can optionally write a tests for that:

class Users::OmniauthCallbacksControllerTest < ActionController::TestCase
    include Devise::Test::ControllerHelpers

    setup do
        @request.env["devise.mapping"] = Devise.mappings[:account]
        @request.env["omniauth.auth"] = discord_oauth2_mock
        OmniAuth.config.test_mode = true
    end

    teardown do
        OmniAuth.config.test_mode = false
    end

    test 'authorize user from discord' do
        assert_not @controller.current_user
        post :discord, params: { code: 'H7buenBzdnlKavjCdG6TWzNLgjrF5p' }
        assert_redirected_to <your_app_main_path>
        assert_not_nil @controller.current_user
        assert @controller.current_user.provider == 'discord'
    end

    ... more cases below

    private
        def discord_oauth2_mock
            OmniAuth.config.mock_auth[:discord] = OmniAuth::AuthHash.new({
                provider: 'discord',
                uid: '940987618345254912',
                info: {
                    email: 'fakeemail@gmail-fake.com',
                    username: "David",
                    image: 'https://cdn.discordapp.com/avatars/940987618345254912/c62c9b46dc9e877fff993bbe56ee7452'
                },
                credentials: {
                    token: 'abcdefgh12345',
                    refresh_token: '12345abcdefgh',
                    expires_at: DateTime.now
                }
            })
        end
end
Enter fullscreen mode Exit fullscreen mode

Integrating Discord OAuth into your Ruby on Rails application can provide a more seamless and secure way for users to sign up and log in. By following the steps outlined in this tutorial, you can have your own "Login with Discord" button up and running in no time.

💖 💪 🙅 🚩
sectasy0
sectasy

Posted on February 5, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related