[Rails]deviseとomniauthによるgithubログイン
はじめに
railsのアプリにgithubアカウントでログインする機能を入れていきます。
devise
を使ってログイン機能を完成した状態からやっていきます。
Deviseの公式Wikiにfacebook
アカウントを使ったログインドキュメントがありますので参考させていただきます。
環境:
Rails 7.0.8
ruby 3.2.1
Gemをインストールする
gem 'omniauth'
gem 'omniauth-github'
gem 'omniauth-rails_csrf_protection'
group :development, :test do
gem 'dotenv-rails'
end
bundle install
omniauth
と omniauth-github
omniauth
と omniauth-github
は、Omniauthフレームワークを使用してGitHubの認証を実装するためのGemです。
omniauth
は、Rubyアプリケーションに対して外部認証(OAuth、OAuth2など)を簡単に統合するためのフレームワークです。Omniauthを使用することで、複数のプロバイダー(GitHub、Google、Facebookなど)との認証フローを統一的に扱うことができます。
omniauth-github
は、GitHubを使用した認証をOmniauthでサポートするためのGemです。このGemを使用すると、GitHubのOAuth認証プロバイダーを簡単に統合できます。
omniauth-rails_csrf_protection
omniauth-rails_csrf_protection
は、OmniauthとRailsのCSRF(Cross-Site Request Forgery)トークンの保護を統合するためのGemです。
CSRFトークンは、Webアプリケーションのセキュリティを強化するために使用されます。Omniauthは、外部認証プロバイダーとの連携時にもCSRFトークンの保護を提供しますが、デフォルトではOmniauthのコールバックはCSRFトークンの検証をスキップします。
omniauth-rails_csrf_protection
Gemを使用することで、OmniauthのコールバックでCSRFトークンの検証を有効にすることができます。これにより、外部プロバイダーからのリクエストに対してもCSRFトークンの保護を適用することができます。
dotenv-rails
dotenv-rails
は、Railsで環境変数を管理するためのgemです。.env
ファイルを使用して、アプリケーションの構成や設定に関する情報を保存し、それらの情報を環境変数として読み込むことができます。
.env
ファイルは、アプリケーション内のさまざまな設定値(データベースの接続情報、APIキー、認証情報など)を格納するために使用されます。これにより、アプリケーションの構成を変更する際に簡単に設定を変更できます。
dotenv-rails
ライブラリを使用すると、.env
ファイルから環境変数を読み込むための自動的な仕組みが提供されます。これにより、Rails アプリケーション内で環境変数を使用する際に簡潔なコードを書くことができます。
OAuth
アプリを作成する
こちらのURLからgithubにて新しいOAuth
アプリを作成します。
アプリの名前、紹介、ホームページURLとコールバックURLを指定します。
Client ID
とClient secrets
は認証に必要です。
OAuth
とは
OAuth(オーオース)は、Webサービス間でのユーザー認証やアクセス許可のためのオープンスタンダードなプロトコルです。
OAuthを使用することで、ユーザーは自身の情報を共有することなく、他のサービスやアプリケーションにアクセス権を付与できます。
例えば、ユーザーがGoogleアカウントを使用してあるアプリに簡単にログインできるようになっています。
アプリケーションはユーザーのGoogleアカウントの資格情報を直接受け取ることなく、認証およびアクセス権限を取得できます。
ユーザーも、パスワードや個人情報をアプリケーションに提供することなく、Googleアカウントを使用して簡単にサービスを利用することができます。
OAuthのメリットは、ユーザーが複数のサービスやアプリケーションを利用する際に、一つのアカウントを共有できるため、パスワードの再利用や管理の負担を軽減できる点です。また、開発者側にとっても、セキュリティやアクセス制御の面で利点があります。
.env
ファイルを作成する
先ほど取得したClient ID
とClient secrets
に書き換えます。
GITHUB_ID = 'CLIENT_ID'
GITHUB_SECRET = 'CLIENT_SECRET'
これにより、環境変数の値を直接参照することができます。
.env.development
や .env.production
のような環境別の .env
ファイルを作成し、それぞれの環境に固有の設定を格納することもできます。
.gitignore
に追加する
.env
ファイルには機密情報(パスワード、秘密鍵など)を含めてますので、github上にアップされないようにソースコードから除外します。
omniauth
プロバイダーを追加する
Devise.setup do |config|
config.omniauth :github, ENV['GITHUB_ID'], ENV['GITHUB_SECRET'], scope: 'user,public_repo'
end
omniauth.rb
を作成しない
omniauth.rb
を作成しプロバイダーの情報を記入する記事もありましが、devise.rb
に記入した場合別でファイルを作成する必要がないです。
以下のエラーが出ます。
Authorization
テーブルを作成する
ユーザーテーブルにカラムを追加することもできますが、プロバイダーを増やす場合まとめて管理したいためAuthorization
テーブルを作ることにしました。
UID
はUser Identifier
の略称で、ユーザーを一意に識別するための識別子です。WebアプリケーションやAPIなどのユーザー認証システムでは、ユーザーを一意に識別するためにUID
が使用されることがあります。ユーザーアカウントに一意のUID
が割り当てられ、ユーザーを特定するために使用されます。
bin/rails generate migration CreateAuthorizations user:references provider:string uid:string name:string email:string
invoke active_record
create db/migrate/20230714071722_create_authorizations.rb
class CreateAuthorizations < ActiveRecord::Migration[6.1]
def change
create_table :authorizations do |t|
t.references :user, null: false, foreign_key: true
t.string :provider
t.string :uid
t.string :name
t.string :email
t.timestamps
end
end
end
bin/rails db:migrate
Running via Spring preloader in process 9887
== 20230714071722 CreateAuthorizations: migrating =============================
-- create_table(:authorizations)
-> 0.0040s
== 20230714071722 CreateAuthorizations: migrated (0.0041s) ====================
User
モデルに設定を追加する
:omniauthable
モジュールを追加しています。
omniauth_provider
に:github
を指定します。
class User < ApplicationRecord
devise :omniauthable, omniauth_providers: %i[github]
has_many :authorizations, dependent: :destroy
end
Authorization
モデルを作成する
provider
とuid
の組み合わせに対して一意の制約を付けます。
class Authorization < ApplicationRecord
belongs_to :user
validates :uid, uniqueness: { scope: :provider }
end
routes.rb
を編集する
プロバイダーからのリダイレクトやコールバックを受け取り、認証情報を検証し、ユーザーを作成またはサインインさせるなどのアクションを実行するためコントローラーが必要です。
users/
ディレクトリ内にomniauth_callbacks
コントローラーを作成します。
Rails.application.routes.draw do
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
end
コールバック用コントローラーを設定する
bin/rails g controller users::omniauth_callbacks
create app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_action :verify_authenticity_token, only: :github
def github
# ユーザー情報を取得
@user = User.from_omniauth(request.env["omniauth.auth"])
# ユーザーを検索または作成
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
githubの認証情報が
request.env['omniauth.auth']
で取得され、User.from_omniauth
メソッドを使用してユーザーを検索または作成しています。
User.from_omniauth
メソッドは、渡された認証情報をもとに、ユーザーを検索または作成するメソッドです。例えば、既存のユーザーとのマッチングやプロバイダーから提供された情報を元に新しいユーザーを作成するなどの処理が含まれます。user.rb
で取得したユーザー情報を@user
に代入されるようにします。
@user.persisted?
がtrue
の場合、ユーザーが正常に作成または検索されたことを意味し、sign_in_and_redirect
メソッドを使用してユーザーをサインインさせ、リダイレクトします。set_flash_message
は、フラッシュメッセージを設定します。
@user.persisted?
がfalse
の場合、ユーザーの作成に問題が発生したことを意味し、new_user_registration_url
にリダイレクトします。このURLは、ユーザー登録ページへのリンクです。
User
モデルにUser.from_omniauth
を作成する
class User < ApplicationRecord
...
def self.from_omniauth(auth)
authorization = Authorization.find_or_initialize_by(provider: auth.provider, uid: auth.uid)
authorization.assign_attributes(name: auth.info.name, email: auth.info.email)
where(email: auth.info.email).first_or_initialize.tap do |user|
user.user_name = auth.info.name
user.remote_profile_url = auth.info.image if auth.info.image.present?
user.save!
user.authorizations << authorization unless user.authorizations.exists?(provider: auth.provider, uid: auth.uid)
end
end
end
Active Storageを使ってユーザー画像を保存する場合
image_url = URI.open(auth.info.image)
user.avatar.attach(io: image_url, filename: "#{user.name}_avatar.jpg", content_type: image_url.content_type )
Authorization.find_or_initialize_by(provider: auth.provider, uid: auth.uid)
:Authorization
モデルを認証プロバイダーとUIDで検索し、存在する場合は取得し、存在しない場合は新しいインスタンスを作成します。
find_or_initialize_by
は、検索結果が存在しない場合、新しいレコードを初期化しますが、データベースには保存されません。そのため、メソッドが呼び出されても、データベースへの変更は行われません。一方、find_or_create_by
は、検索結果が存在しない場合、新しいレコードを作成してデータベースに保存します。
auth.info.image
はプロフィールのURLです。条件としてauth.info.image.present?
を使用して、URLが存在する場合のみremote_profile_url
メソッドを呼び出します。CarrierWaveの
remote_profile_urlメソッドを使用して、ユーザーのGitHubプロフィール画像を取得および保存することができます。Userモデルの
profile`属性に対してCarrierWaveの設定が適切に行われていることが前提です。
CarrierWaveの
remote_url
メソッドは、リモートの画像のURLを指定してモデルの属性に設定するためのメソッドです。
authorization.assign_attributes(name: auth.info.name, email: auth.info.email)
:取得または作成したAuthorization
インスタンスの属性に、認証情報から取得した名前とメールアドレスを割り当てます。
assign_attributes
メソッドは、与えられた属性を一括で割り当てるために使用されます。通常、複数の属性を一度に変更する必要がある場合に便利です。assign_attributes メソッドを使用すると、属性の変更がオブジェクトに一括で反映されますが、データベースへの保存は行われません。
where(email: auth.info.email).first_or_initialize.tap do |user|
:メールアドレスを使用してユーザーを検索します。存在する場合は取得し、存在しない場合は新しいインスタンスを初期化します。この部分はwhere
で条件に一致するユーザーを検索し、first_or_initialize
で最初の検索結果を返すか新しいインスタンスを初期化します。
user.save!
:ユーザーを保存します。
user.authorizations << authorization unless user.authorizations.exists?(provider: auth.provider, uid: auth.uid)
:ユーザーに対して関連するAuthorization
レコードを追加しますが、既に同じプロバイダーとUIDの組み合わせで存在する場合は追加しません。この行は、ユーザーが複数の認証プロバイダーを持つ場合に、関連する認証情報を追加するためのものです。
githubでログインする
サーバーを再起動し、Sign in with Github
のボタンがあることを確認します。
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %><br />
<% end %>
<% end %>
早速ログインしてみます。
フラッシュメッセージが表示されること、Authorization
とUser
テーブルにユーザーが保存されることができましたので大丈夫そうですね。
Started POST "/users/auth/github" for ::1 at 2023-07-16 11:51:01 +0900
D, [2023-07-16T11:51:01.600188 #31792] DEBUG -- omniauth: (github) Request phase initiated.
Started GET "/users/auth/github/callback?code=**************&state=**************" for ::1 at 2023-07-16 11:51:16 +0900
D, [2023-07-16T11:51:16.160490 #31792] DEBUG -- omniauth: (github) Callback phase initiated.
Processing by Users::OmniauthCallbacksController#github as HTML
Parameters: {"code"=>"**************", "state"=>"**************"}
Authorization Load (0.1ms) SELECT "authorizations".* FROM "authorizations" WHERE "authorizations"."provider" = ? AND "authorizations"."uid" = ? LIMIT ? [["provider", "github"], ["uid", "**************"], ["LIMIT", 1]]
↳ app/models/user.rb:25:in `from_omniauth'
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? ORDER BY "users"."id" ASC LIMIT ? [["email", "**************"], ["LIMIT", 1]]
↳ app/models/user.rb:28:in `from_omniauth'
TRANSACTION (0.0ms) begin transaction
↳ app/models/user.rb:29:in `block in from_omniauth'
User Exists? (0.1ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ? [["email", "**************"], ["id", 12], ["LIMIT", 1]]
↳ app/models/user.rb:29:in `block in from_omniauth'
User Update (0.3ms) UPDATE "users" SET "updated_at" = ?, "profile" = ? WHERE "users"."id" = ? [["updated_at", "2023-07-16 15:26:09.707391"], ["profile", "139666959.png"], ["id", 12]]
↳ app/models/user.rb:32:in `block in from_omniauth'
TRANSACTION (0.0ms) commit transaction
↳ app/models/user.rb:29:in `block in from_omniauth'
Authorization Exists? (0.1ms) SELECT 1 AS one FROM "authorizations" WHERE "authorizations"."user_id" = ? AND "authorizations"."provider" = ? AND "authorizations"."uid" = ? LIMIT ? [["user_id", 12], ["provider", "github"], ["uid", "**************"], ["LIMIT", 1]]
↳ app/models/user.rb:30:in `block in from_omniauth'
TRANSACTION (0.0ms) begin transaction
↳ app/controllers/users/omniauth_callbacks_controller.rb:8:in `github'
User Update (0.3ms) UPDATE "users" SET "updated_at" = ?, "sign_in_count" = ?, "current_sign_in_at" = ?, "last_sign_in_at" = ? WHERE "users"."id" = ? [["updated_at", "2023-07-16 11:51:17.078845"], ["sign_in_count", 4], ["current_sign_in_at", "2023-07-16 11:51:17.078685"], ["last_sign_in_at", "2023-07-16 11:49:25.521655"], ["id", 12]]
↳ app/controllers/users/omniauth_callbacks_controller.rb:8:in `github'
TRANSACTION (1.0ms) commit transaction
↳ app/controllers/users/omniauth_callbacks_controller.rb:8:in `github'
Redirected to http://localhost:3000/
Completed 302 Found in 17ms (ActiveRecord: 1.9ms | Allocations: 8394)
終わりに
ドキュメントを参考しながら一通りgithubでのログイン機能ができました。
他のプロバイダーを試してみたいと思います。
Discussion