🦓

[Rails]deviseとomniauthによるgithubログイン

2023/07/15に公開

はじめに

railsのアプリにgithubアカウントでログインする機能を入れていきます。
deviseを使ってログイン機能を完成した状態からやっていきます。

Deviseの公式Wikiにfacebookアカウントを使ったログインドキュメントがありますので参考させていただきます。
https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview

環境:

Rails 7.0.8
ruby 3.2.1

Gemをインストールする

Gemfile
gem 'omniauth'
gem 'omniauth-github'
gem 'omniauth-rails_csrf_protection'
group :development, :test do
  gem 'dotenv-rails'
end
bundle install

omniauthomniauth-github

omniauthomniauth-github は、Omniauthフレームワークを使用してGitHubの認証を実装するためのGemです。

omniauth は、Rubyアプリケーションに対して外部認証(OAuth、OAuth2など)を簡単に統合するためのフレームワークです。Omniauthを使用することで、複数のプロバイダー(GitHub、Google、Facebookなど)との認証フローを統一的に扱うことができます。

omniauth-github は、GitHubを使用した認証をOmniauthでサポートするためのGemです。このGemを使用すると、GitHubのOAuth認証プロバイダーを簡単に統合できます。

https://github.com/omniauth/omniauth
https://github.com/omniauth/omniauth-github

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アプリを作成します。
https://github.com/settings/developers

アプリの名前、紹介、ホームページURLとコールバックURLを指定します。

Client IDClient secretsは認証に必要です。

OAuthとは

OAuth(オーオース)は、Webサービス間でのユーザー認証やアクセス許可のためのオープンスタンダードなプロトコルです。
OAuthを使用することで、ユーザーは自身の情報を共有することなく、他のサービスやアプリケーションにアクセス権を付与できます。
例えば、ユーザーがGoogleアカウントを使用してあるアプリに簡単にログインできるようになっています。
アプリケーションはユーザーのGoogleアカウントの資格情報を直接受け取ることなく、認証およびアクセス権限を取得できます。
ユーザーも、パスワードや個人情報をアプリケーションに提供することなく、Googleアカウントを使用して簡単にサービスを利用することができます。
OAuthのメリットは、ユーザーが複数のサービスやアプリケーションを利用する際に、一つのアカウントを共有できるため、パスワードの再利用や管理の負担を軽減できる点です。また、開発者側にとっても、セキュリティやアクセス制御の面で利点があります。

.envファイルを作成する

先ほど取得したClient IDClient secretsに書き換えます。

.env
GITHUB_ID = 'CLIENT_ID'
GITHUB_SECRET = 'CLIENT_SECRET'

これにより、環境変数の値を直接参照することができます。
.env.development.env.production のような環境別の .env ファイルを作成し、それぞれの環境に固有の設定を格納することもできます。

.gitignoreに追加する

.envファイルには機密情報(パスワード、秘密鍵など)を含めてますので、github上にアップされないようにソースコードから除外します。

omniauthプロバイダーを追加する

config/initializers/devise.rb
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テーブルを作ることにしました。

UIDUser 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
db/migrate/xxx_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を指定します。

app/models/user.rb
class User < ApplicationRecord
    devise :omniauthable, omniauth_providers: %i[github]
    has_many :authorizations, dependent: :destroy
end

Authorizationモデルを作成する

provideruidの組み合わせに対して一意の制約を付けます。

app/models/authorization.rb
class Authorization < ApplicationRecord
  belongs_to :user

  validates :uid, uniqueness: { scope: :provider }
end

routes.rbを編集する

プロバイダーからのリダイレクトやコールバックを受け取り、認証情報を検証し、ユーザーを作成またはサインインさせるなどのアクションを実行するためコントローラーが必要です。
users/ディレクトリ内にomniauth_callbacksコントローラーを作成します。

config/routes.rb
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
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を作成する

app/models/user.rb
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メソッドを呼び出します。CarrierWaveremote_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のボタンがあることを確認します。

app/views/devise/shared/_links.html.erb
<%- 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 %>

早速ログインしてみます。
フラッシュメッセージが表示されること、AuthorizationUserテーブルにユーザーが保存されることができましたので大丈夫そうですね。

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でのログイン機能ができました。
他のプロバイダーを試してみたいと思います。

https://fuga-ch85.hatenablog.com/entry/2021/04/10/075536
https://github.com/carrierwaveuploader/carrierwave#uploading-files-from-a-remote-location
http://blog.applest.net/article/20140925-carrierwave-uploads-image-from-url/
https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/building-a-login-with-github-button-with-a-github-app

Discussion