How to Implement Login with Discord feature in Rails 7 using Devise
Tags: rails, discord, webdev, tutorial
Published: 2023-02-05 12:03:58

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](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`

```ruby
gem 'devise', '~> 4.8.1'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-discord', '~> 1.0.0'
```

### Step 3: Configure the `omniauth-discord` gem

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

```sh
EDITOR=vim rails credentials:edit
```

```yml
discord:
	client_id: 1068564434567456784
	secret: BCf6heHXdcCc5Lwy5YL5R5BJ6SDm6QK2
```

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

```ruby
discord_oauth = Rails.application.credentials.discord
config.omniauth :discord, discord_oauth[:client_id], discord_oauth[:secret], scope: 'email identify'
```

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

```ruby
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
```

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

```ruby
gem 'carrierwave', '~> 2.2.3'
gem 'carrierwave-i18n', '~> 0.2.0'
```

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

```ruby
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
```

Next, mount uploader to your model.
```ruby
mount_uploader :avatar, AvatarUploader
```

> **_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.

```ruby
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
```

```ruby
image_available = Faraday.get(auth.info.image).status == 200
```

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.

```ruby
account.remote_image_url = auth.info.image if image_available
```

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:

```ruby
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
```

### 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:

```ruby
= button_to("Log in with Discord", user_discord_omniauth_authorize_path)
```

### 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:
```ruby
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
```

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.