GitHubログイン（OmniAth）で実装

# はじめに
今回は、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 の説明](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)
こちらにわかりやすく説明されています。


# 開発環境での実装
まずは開発環境で実装していきたいと思います。

# GitHubでClient IDとClients secretsを取得する
まずは認証に必要となってくる、「Client ID」と「Clients secrets」を取得します。
こちらの方法については、
[GitHubのClient IDとClient secretsを取得する手順](https://fuga-ch85.hatenablog.com/entry/2021/04/04/164302)
が分かりやすかったので参考にして作成してください。

# dotenvで情報を保存する
Client IDとClients secretsのようなクレデンシャルなものは、そのままファイルに記述すると情報漏洩に繋がりかねないのでよろしくありません。
なので今回は、`dotenv-rails`という環境変数を管理するとこができるgemを使用して、情報を管理していきたいと思います。

```ruby: Gemfile
gem 'dotenv-rails'
```

Gemfileに`dotenv-rails`を追加して、`bundle install`を実行します。


```
$ touch .env
```
続いて、アプリケーションディレクトリに`.env`ファイルを作成します。

```ruby: .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のインストールをします。

```ruby:Gemfile
gem 'omniauth'
gem 'omniauth-github'
gem 'omniauth-rails_csrf_protection'
```

Gemfileに上記のgemを追加して、`bundle install`を実行します。

GitHubのClient IDとClient secretを設定します。
```ruby:config/initializers/devise.rb
config.omniauth :github, ENV['GITHUB_ID'], ENV['GITHUB_SECRET']
```

UserモデルにOmniAuthで利用するカラムを追加するためのマイグレーションファイルを作成します。
```
$ rails g migration AddOmniauthToUsers uid:string provider:string 
```
生成されたマイグレーションファイルの編集をします。
```ruby:
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に合わせて、モデル側にもバリデーションの設定をしていきます。
```ruby:app/models/user.rb
  validates :uid, uniqueness: { scope: :provider }, if: -> { uid.present? }
```
uidが存在する場合に限り、同じuidとproviderの組み合わせが一意であることを保証するバリデーションを設定しています。

deviseのモジュール`:omniauthable`を追加していきます。
さらに`omniauth_provider`に`:github`を指定して、githubでのOAuthに対応させることができます。
```ruby:app/models/user.rb
devise :database_authenticatable, :confirmable, :recoverable, :registerable,
       :rememberable, :trackable, :timeoutable, :validatable, :Lockable, :omniauthable, omniauth_providers: %i[github]

```

次に認証後のcallback処理のルーティングを追加していきます。
```ruby:config/routes.rb
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
```

`devise_for :users`はdevise導入時に自動生成させるルーティングでdeviseに関係するルーティングを記述するところ。
上記の記述にすることで、`users/omniauth_callbacks`コントローラーを使用する、という指定をしています。


続いてコントローラーの記述をします。
```ruby:app/controllers/users/omniauth_callbacks_controller.rb
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`メソッドを定義します。
```app/models/user.rb
# 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認証の実装はほとんど完成です。

実際に認証機能を試してみると。。。

![スクリーンショット 2024-02-09 23.17.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335067/b5bad287-b6fa-a960-abb3-375549ecd527.png)
エラー画面ではないけど、`Not found. Authentication passthru.`と言うメッセージが。
ユーザーが認証プロバイダー（例: GitHub）でのログインを試みているが、何らかの理由で認証が失敗しているというエラーメッセージみたいです。

こちらはエラーが出る人と出ない人がいるみたいなのですが、私はどっぷりハマってしまいました。
こちらのエラーの解決策は、
[SNS認証におけるNot found. Authentication passthru.エラーについて](https://qiita.com/Sugizou0215/items/9fb838793dc8502b97d6)
の記事を参考にさせていただきました。

```ruby:config/initializers/omniauth.rb
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を埋める処理と、パスワードなしでユーザー編集できるようにする処理を加えていきたいと思います。

```ruby:app/users/registrations_controller.rb
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

```
```ruby:app/models/user.rb
  def self.create_unique_string
    SecureRandom.uuid
  end
```
この記述を追加することによって、問題を解消することができると思います。


最後に、github認証でのログインの場合のメールのスキップ処理を追加します。
通常ログインの場合は、メールアドレスでの認証ができた後にログインができるように設定されていますが、github認証でのログインの際にはメールアドレスでの認証をスキップしたいので、その処理を追加していきます。

```ruby:app/models/user.rb
  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
```

```ruby:追加するコード
if user.persisted? || auth.provider == 'github'
  user.skip_confirmation! if auth.provider == 'github'
  user.save
end
```

上記のコードを追加することによって、github認証の際のメール認証をスキップすることができました。


# 最後に
これでGitHubログインの実装ができたと思います。
今後自分が実装する時の備忘録と今後どなたかのお役に立てたらと思います。

最後まで見ていただきありがとうございました。