5
7

RailsでDiscordログインを実装する方法

Posted at

はじめに

この記事では、Rails7のOAuth機能を使ってDiscordログインを実装する方法について書きます。

  • Discordログインを実装したい
  • omniauthを使いたいけど、使い方がわからない

と言った人はぜひ読んでくださいね。
それでは、順番に解説していきます。

Discordログインを実装する手順

RailsでDiscordログインを実装する全体像を先に解説します。

  1. Discordの開発者用画面で設定キーを取得する
  2. .envファイルを設定する
  3. omniauthというgemを導入する
  4. ログインの処理を記述する
  5. ビューにリンクを追加する
  6. テストコードを書く

以上になります。
それでは、1つずつ解説します!

1. Discordの開発者用画面で設定キーを取得する

Discord Developer Portalにアクセス&ログインしてください
画像付きで解説しますが、全体としては

  • CLIENT_ID
  • CLIENT_SECRET

の2つの設定キーを取得して、リダイレクトURLを設定していきます。

Image from Gyazo

Image from Gyazo

Image from Gyazo

Image from Gyazo

Image from Gyazo

これでひとまず

  • CLIENT_ID
  • CLIENT_SECRET

は取得できました!後程、それらをENVファイルに記述してください。
Image from Gyazo


次はリダイレクトURLを設定していきます

リダイレクト先のURLは/auth/discord/callbackというパスになるようにしてください
例えばローカルならhttp://localhost:3000/auth/discord/callbackになります。

先ほどのCLIENT_IDを設定した画面で作業してください
Image from Gyazo

Image from Gyazo

以上で設定は完了です。次はその設定をenvファイルに書いていきましょう。

2. envファイルに設定する

次は、環境変数に設定しましょう。
envファイルでなくても構いませんが、それぞれでカスタマイズお願いします。

.envファイルをプロジェクトルートパス直下(デフォルトのGemfileと同じ位置)に置いてください
.をつけることに注意です。

先に.envファイルの中身を書いておきます。

.env
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=

上にCLIENT_ID, 下にCLIENT_SECRETを入れてください。

envファイル用追加設定(必要なら)

上記でenvファイル自体は完了になりますが、もしenvファイルを新たに作成した場合は新たにgemを追加しましょう。

Gemfileに下記を追加してください

Gemfile
gem 'dotenv-rails'

コマンドを実行してください

bundle install

必要ならrails sなどサーバーを再起動してください。これだけでOKです。

3. omniauthを導入する

Gemfile内に下記を追記しましょう。

Gemfile
gem 'omniauth-discord'
gem 'omniauth-rails_csrf_protection'

コマンド実行です。

bundle install

次に config/initializers/omniauth.rbファイルを作成して設定を追記します。

omniauth.rb
# frozen_string_literal: true

Rails.application.config.middleware.use OmniAuth::Builder do
  # scopeは各自必要な値に修正してください。(ユーザー名とかならそのままでいけます)
  provider :discord, ENV['DISCORD_CLIENT_ID'], ENV['DISCORD_CLIENT_SECRET'], scope: 'identify'

  OmniAuth.config.on_failure = proc { |_env| [302, { 'Location' => '/auth/failure', 'Content-Type' => 'text/html' }, []] }
end

ついでに、config/routes.rbファイルに以下を追記してください。

# コールバック先(ここにログインの処理を書く)
get "auth/:provider/callback", to: "user_sessions#callback"

# キャンセルなど、失敗した時のルーティング
get 'auth/failure', to: redirect('/')

ひとまずこれでOKです。次から、ログイン処理を実装していきましょう。

4. ログインの処理を記述する

リダイレクト先であるコントローラーにcallbackアクションを追記しましょう。
僕の場合はsessions_controller.rbファイルになります

下記はコードの意図ですが、理解しておくとよさそうです。

  • request.env['omniauth.auth'] → Discordユーザー名などの情報が入っている
  • reset_session → セキュリティを考慮して、ログイン前にセッションをリセット
users/sessions_controller.rb
# frozen_string_literal: true

class Users::SessionsController < ApplicationController
  def callback
    auth_info = request.env['omniauth.auth']
    if (user = User.find_or_create_from_discord_info(auth_info))
      reset_session
      log_in user
    end

    redirect_to root_path, notice: 'ログインしました'
  end
...

Userモデルは下記のようになります。

models/user.rb
# frozen_string_literal: true

class User < ApplicationRecord
  def self.find_or_create_from_discord_info(discord_info)
    User.find_or_create_by(uid: discord_info.uid) do |user|
      user.update!(
        uid: discord_info.uid,
        name: discord_info.info.name,
        image: discord_info.info.image
      )
    end
  end
...

また、もしログインしないとアクセスできないなどbefore_actionしている場合はskip_before_actionしてください。

また、ログインのロジックなどについて参考ファイルも載せておきます。

ログインなどの参考ファイル

omniauthを使っていると独自でログインのロジックを組む必要があるかと思います。
僕はそれをSessionHelperモジュールで実装し、ApplicationControllerで読み込んでいます。

log_inメソッドを定義しているのは下記のファイルの一部です。
他のメソッドもあえて載せてますので参考になればと思います。

helpers/sessions_helper.rb
# frozen_string_literal: true

module SessionsHelper
  def current_user
    return unless (user_id = session[:user_id])

    @current_user ||= User.find_by(id: user_id)
  end

  def logged_in?
    !current_user.nil?
  end

  def current_user?(user)
    user && user == current_user
  end

  def log_in(user)
    session[:user_id] = user.id
  end

  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end
application_controller.rb
# frozen_string_literal: true

class ApplicationController < ActionController::Base
  include SessionsHelper
  
...
end

5. ビューにリンクを追加する

次はビューファイルにログインボタンを設定しましょう。

ログインボタン
<%= button_to '/auth/discord', method: :post, data: { turbo: false } do %>
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-discord" viewBox="0 0 16 16">
    <path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z" />
  </svg>
  Discord アカウントでログイン
<% end %>

少し解説します。

POST通信について

omniauth-rails_csrf_protectionというgemの影響でCSRF攻撃を防ぐため、method: :postを記述しています。

Turboオフについて

ログインボタンではturboをオフにせよとのことでした。
なので、data: { turbo: false }を追記しています

turboってなんぞや?という人も今はふーんで構いません。

6. テストコードを書く

ロジックを書いたら、しっかりテストコードも書きましょう。
フレームワークはRSpecを使って書きました。

まずはモデルテストになります。

models/user_spec.rb
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe User do
  describe '.find_or_create_from_discord_info' do
    let(:login_user) { User.find_or_create_from_discord_info(auth_info) }
    let(:auth_info) { OmniAuth::AuthHash.new({ provider: 'discord', uid: '123456', info: { name: 'yocchan', image: 'https://discord.cdn.example.com' } }) }

    context '新しく作成されるユーザーの場合' do
      it 'ユーザーが新規作成される' do
        expect(login_user.uid).to eq '123456'
        expect(login_user.name).to eq 'yocchan'
        expect(login_user.image).to eq 'https://discord.cdn.example.com'
      end
    end

    context 'すでに作成されているユーザーの場合' do
      let!(:user) { create(:user, uid: '123456') }

      it 'すでに作成ずみのユーザーが返る' do
        expect(login_user).to eq user
      end
    end
  end

...

また、システムテストは下記になります。

system/users/sessions_spec.rb
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Users::Sessions' do
  let!(:user) { create(:user) }
  before { OmniAuth.config.test_mode = true }

  describe '#callback' do
    before { OmniAuth.config.add_mock(:discord, { uid: user.uid, info: { name: user.name, image: user.image } }) }

    context '一度ログインしたことあるユーザーの場合' do
      it 'ログインし、タイムライン画面に遷移する' do
        visit new_users_session_path
        click_on 'Discord アカウントでログイン'
        expect(page).to have_css '.text-success', text: 'ログインしました'
      end
    end
  end

  describe '#failure' do
    before { OmniAuth.config.mock_auth[:discord] = :invalid_credentials }

    it 'ログインが失敗する' do
      visit new_users_session_path
      click_on 'Discord アカウントでログイン'
      expect(page).to have_link, 'Discord アカウントでログイン'
    end
  end
end

もし、テストが落ちたりしたら、微修正などお願いします。
ざっとこれで実装完了です!お疲れ様でした。

参考にさせていただいた記事

2つの記事がとっても参考になりました。
ありがとうございました!

いいねや、ストックしてくださると嬉しいです。
それでは!

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7