はじめに
この記事では、Rails7のOAuth機能を使ってDiscordログインを実装する方法について書きます。
- Discordログインを実装したい
- omniauthを使いたいけど、使い方がわからない
と言った人はぜひ読んでくださいね。
それでは、順番に解説していきます。
Discordログインを実装する手順
RailsでDiscordログインを実装する全体像を先に解説します。
- Discordの開発者用画面で設定キーを取得する
-
.env
ファイルを設定する -
omniauth
というgemを導入する - ログインの処理を記述する
- ビューにリンクを追加する
- テストコードを書く
以上になります。
それでは、1つずつ解説します!
1. Discordの開発者用画面で設定キーを取得する
Discord Developer Portalにアクセス&ログインしてください
画像付きで解説しますが、全体としては
CLIENT_ID
CLIENT_SECRET
の2つの設定キーを取得して、リダイレクトURLを設定していきます。
これでひとまず
CLIENT_ID
CLIENT_SECRET
は取得できました!後程、それらをENVファイルに記述してください。
次はリダイレクトURLを設定していきます
リダイレクト先のURLは/auth/discord/callback
というパスになるようにしてください
例えばローカルならhttp://localhost:3000/auth/discord/callback
になります。
以上で設定は完了です。次はその設定をenv
ファイルに書いていきましょう。
2. envファイルに設定する
次は、環境変数に設定しましょう。
envファイルでなくても構いませんが、それぞれでカスタマイズお願いします。
.env
ファイルをプロジェクトルートパス直下(デフォルトのGemfile
と同じ位置)に置いてください
.
をつけることに注意です。
先に.env
ファイルの中身を書いておきます。
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
上にCLIENT_ID
, 下にCLIENT_SECRET
を入れてください。
envファイル用追加設定(必要なら)
上記でenvファイル自体は完了になりますが、もしenvファイルを新たに作成した場合は新たにgem
を追加しましょう。
Gemfileに下記を追加してください
gem 'dotenv-rails'
コマンドを実行してください
bundle install
必要ならrails s
などサーバーを再起動してください。これだけでOKです。
3. omniauth
を導入する
Gemfile
内に下記を追記しましょう。
gem 'omniauth-discord'
gem 'omniauth-rails_csrf_protection'
コマンド実行です。
bundle install
次に config/initializers/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
→ セキュリティを考慮して、ログイン前にセッションをリセット
# 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モデルは下記のようになります。
# 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
メソッドを定義しているのは下記のファイルの一部です。
他のメソッドもあえて載せてますので参考になればと思います。
# 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
# 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
を使って書きました。
まずはモデルテストになります。
# 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
...
また、システムテストは下記になります。
# 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つの記事がとっても参考になりました。
ありがとうございました!
いいねや、ストックしてくださると嬉しいです。
それでは!