はじめに
今回は、deviseで実装したユーザー認証機能にomniauthでGitHubログインを導入していく方法をまとめていきたいと思います。
今回自分が実装するにあたってかなり手こずったので、誰かのお役に立てたらと思います。
※今回GitHubログインの実装するにあたって、deviseでの認証機能の実装ができていることを前提としています。
環境設定
- Ruby 3.2.1
- Rails 7.0.0
- docker
- 本番環境はherokuにデプロイ
仕様
- deviseのomniauthableの機能でgithubログインを実装する
- omniauth関連のgemは適宜使用してよいものとする
- localhost:3000/users/sign_in,localhost:3000/users/sign_upからgithubログインができるようになること
(herokuデプロイの時は、URLが変わります。)
omniauthについて
Rubyアプリケーションに対して外部認証(OAuth,OAuth2など)を簡単に統合するためのフレームワーク。
OmniAuthを仕様することで、さまざまなプロバイダー(GitHub,Google,Facebookなど)との認証フローを統一的に扱うことができる。
OAuthとは
ユーザーが製品/アプリを承認して、別の製品/アプリ内に保存されているリソースにアクセスできるようにする認可のプロセスのこと。
一番分かりやすい OAuth の説明
こちらにわかりやすく説明されています。
開発環境での実装
まずは開発環境で実装していきたいと思います。
GitHubでClient IDとClients secretsを取得する
まずは認証に必要となってくる、「Client ID」と「Clients secrets」を取得します。
こちらの方法については、
GitHubのClient IDとClient secretsを取得する手順
が分かりやすかったので参考にして作成してください。
dotenvで情報を保存する
Client IDとClients secretsのようなクレデンシャルなものは、そのままファイルに記述すると情報漏洩に繋がりかねないのでよろしくありません。
なので今回は、dotenv-rails
という環境変数を管理するとこができるgemを使用して、情報を管理していきたいと思います。
gem 'dotenv-rails'
Gemfileにdotenv-rails
を追加して、bundle install
を実行します。
$ touch .env
続いて、アプリケーションディレクトリに.env
ファイルを作成します。
GITHUB_ID = 'CLIENT_ID'
GITHUB_SECRET = 'CLIENT_SECRET'
# CLIENT_IDとCLIENT_SECRETには先ほど取得したものに置き換えてください。
ちゃんと取得できているか確認するために、コンソールを使用して確認します。
$ rails c
irb(main):001:0> ENV['GITHUB_ID']
=> "CLIENT_ID"
irb(main):002:0> ENV['GITHUB_SECRET']
=> "CLIENT_SECRET"
先ほど.env
ファイルに保存した値が返ってくればOKです。
omniauthを使ってGitHubログインを実装する
まずはこれから使用したいgemのインストールをします。
gem 'omniauth'
gem 'omniauth-github'
gem 'omniauth-rails_csrf_protection'
Gemfileに上記のgemを追加して、bundle install
を実行します。
GitHubのClient IDとClient secretを設定します。
config.omniauth :github, ENV['GITHUB_ID'], ENV['GITHUB_SECRET']
UserモデルにOmniAuthで利用するカラムを追加するためのマイグレーションファイルを作成します。
$ rails g migration AddOmniauthToUsers uid:string provider:string
生成されたマイグレーションファイルの編集をします。
class AddOmniauthToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :uid, :string
add_column :users, :provider, :string, null: false, default: ""
add_index :users, [:uid, :provider], unique: true
end
end
「uid」と「provider」というカラムを追加します。
そして、2つのカラムにindexを張り、unique制約を付ける。これによって、2つのカラムの組み合わせに対して一意の制約を付けることができます。
さらにDBに合わせて、モデル側にもバリデーションの設定をしていきます。
validates :uid, uniqueness: { scope: :provider }, if: -> { uid.present? }
uidが存在する場合に限り、同じuidとproviderの組み合わせが一意であることを保証するバリデーションを設定しています。
deviseのモジュール:omniauthable
を追加していきます。
さらにomniauth_provider
に:github
を指定して、githubでのOAuthに対応させることができます。
devise :database_authenticatable, :confirmable, :recoverable, :registerable,
:rememberable, :trackable, :timeoutable, :validatable, :Lockable, :omniauthable, omniauth_providers: %i[github]
次に認証後のcallback処理のルーティングを追加していきます。
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
devise_for :users
はdevise導入時に自動生成させるルーティングでdeviseに関係するルーティングを記述するところ。
上記の記述にすることで、users/omniauth_callbacks
コントローラーを使用する、という指定をしています。
続いてコントローラーの記述をします。
module Users
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_action :verify_authenticity_token, only: :github
def github
# ユーザー情報を取得
# request.env["omniauth.auth"]にGitHubから送られてきたデータが入っている
@user = User.from_omniauth(request.env['omniauth.auth'])
# @userがデータベースに保存されているかどうか
if @user.persisted?
# 既存のユーザーがデータベースに保存されている場合、そのユーザーをサインイン状態にしておく
sign_in_and_redirect @user, event: :authentication
# サインインが成功した場合、成功メッセージを設定する
set_flash_message(:notice, :success, kind: 'github') if is_navigational_format?
else # 保存されていない場合
# 一時的にセッションに認証情報を保存する
session['devise.github_data'] = request.env['omniauth.auth'].except(:extra)
# 新しいユーザーの登録ページにリダイレクト
redirect_to new_user_registration_url
end
end
def failure
redirect_to root_path
end
end
end
form_omniauth
メソッドを定義します。
# authの中身はGitHubから送られてくる大きなハッシュ。この中に名前やメアドなどの情報が入っている。
# providerカラムとuidカラムが送られてきたデータと一致するユーザーを探す。
# もしユーザーが見つからない場合は新規作成する。
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create! do |user|
user.email = auth.info.email
# 任意の20文字の文字列を作成する
user.password = Devise.friendly_token[0, 20]
user.name = auth.info.name
user.telephone_number = '00000000000'
user.date_of_birth = '1997-01-01'
end
ここで注意する点があります。
ブロック内で設定するカラムは実際の登録時のバリデーションに合わせないとエラーになります。
私はここでかなり躓きました。
また、deviseでのログイン機能を実装時に作成したデータは一度消しておかないとここでもエラーが発生しました。
これで、github認証の実装はほとんど完成です。
実際に認証機能を試してみると。。。
エラー画面ではないけど、Not found. Authentication passthru.
と言うメッセージが。
ユーザーが認証プロバイダー(例: GitHub)でのログインを試みているが、何らかの理由で認証が失敗しているというエラーメッセージみたいです。
こちらはエラーが出る人と出ない人がいるみたいなのですが、私はどっぷりハマってしまいました。
こちらのエラーの解決策は、
SNS認証におけるNot found. Authentication passthru.エラーについて
の記事を参考にさせていただきました。
Rails.application.config.middleware.use OmniAuth::Builder do
OmniAuth.config.allowed_request_methods = [:post, :get]
end
を追加することによって解消されました。
こちらは、OmniAuthでの認証時にPOSTメソッドを許可する記述です。これがない状態だと、うまくPOSTメソッドが機能しない状態だったのでエラーメッセージが表示されていました。
導入後の問題を解決する
実装は完了したものの、問題が残っています。
- OAuth認証をせずに登録するユーザーは、2人目以降登録できない
- OAuth認証で登録したユーザーはパスワードが自動で設定されるにもかかわらず、パスワードなしでプロフィール編集ができない
この問題を解決していきます。
なぜ2人目以降できないのか?
uidとproviderの組み合わせにunique制約を付けていることで、uid、providerともに空という組み合わせは1人のユーザーしか持つことができないからです。
また、OAuthで登録したユーザーはランダムなパスワードを付与しています(実際のユーザーはパスワードを知り得ない状態)が、ユーザー編集をするにはパスワードが必要な状態になってしまっています。
この問題を解決するために、OAuthを使用しない場合にuidを埋める処理と、パスワードなしでユーザー編集できるようにする処理を加えていきたいと思います。
module Users
class RegistrationsController < Devise::RegistrationsController
protected
# hashをもとにresourceの新しいインスタンスを作る
def build_resource(hash = {})
hash[:uid] = User.create_unique_string
super
end
def update_resource(resource, params)
return super if params['password'].present?
# 現在のパスワードなしでアカウントの更新をする
resource.update_without_password(params.except('current_password'))
end
end
end
def self.create_unique_string
SecureRandom.uuid
end
この記述を追加することによって、問題を解消することができると思います。
最後に、github認証でのログインの場合のメールのスキップ処理を追加します。
通常ログインの場合は、メールアドレスでの認証ができた後にログインができるように設定されていますが、github認証でのログインの際にはメールアドレスでの認証をスキップしたいので、その処理を追加していきます。
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create! do |user|
user.email = auth.info.email
# 任意の20文字の文字列を作成する
user.password = Devise.friendly_token[0, 20]
user.name = auth.info.name
user.telephone_number = '00000000000'
user.date_of_birth = '1997-01-01'
if user.persisted? || auth.provider == 'github'
user.skip_confirmation! if auth.provider == 'github'
user.save
end
end
end
if user.persisted? || auth.provider == 'github'
user.skip_confirmation! if auth.provider == 'github'
user.save
end
上記のコードを追加することによって、github認証の際のメール認証をスキップすることができました。
最後に
これでGitHubログインの実装ができたと思います。
今後自分が実装する時の備忘録と今後どなたかのお役に立てたらと思います。
最後まで見ていただきありがとうございました。