Rails 7にOmniAuthでTwitterログイン機能を搭載

# はじめに

10年ほど前にOmniAuthでTwitterログインをするという機能をいくつかのシステムで実装したのですが、久しぶりに同じことをやってみようとすると時間がかかってしまったので、今回実施した内容をまとめておくことにしました。

以下のバージョンで動作を確認しています。deviseとは連携しないでOmniAuth単体でユーザー認証をします。

- Ruby 3.1.2
- Rails 7.0.4
- OmniAuth 2.1.0
- OmniAuth::Twitter2 0.1.0

# ライブラリのインストール

TwitterのAPIがv2になっていたので、それに対応したStrategyのライブラリを利用します。

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

```ruby:Gemfile
gem "omniauth-twitter2"
gem "omniauth-rails_csrf_protection"
```

```bash
% bundle
```

# Client ID, Client Secretの取得と設定

Twitter Developer Portalでアプリを登録します。

https://developer.twitter.com/en/portal/dashboard

アプリを登録した後で`User authentication settings`から`Client ID`と`Client Secret`を作成します。

`Callback URI`には`http://localhost:3000/auth/twitter2/callback`を設定します。

`EDITOR=vi rails credentials:edit`を実行して`Client ID`と`Client Secret`を保存します。

```yaml
twitter:
  client_id: ********
  client_secret: ****************
```

`omniauth.rb`を作成します。必要な権限に応じて`scope`を変更します。

```ruby:config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter2,
           Rails.application.credentials.twitter[:client_id],
           Rails.application.credentials.twitter[:client_secret],
           callback_path: "/auth/twitter2/callback",
           scope: "tweet.read users.read"

  OmniAuth.config.on_failure =
    Proc.new { |env| OmniAuth::FailureEndpoint.new(env).redirect_to_failure }
end
```

# ユーザーモデルとセッションコントローラーの作成

## ユーザーモデル

以下の例では`provider`と`uid`と`name`のみを格納していますが、作成するアプリに応じてデータ項目を増やすことになります。

```bash
% rails g model User provider:string uid:string name:string
% rake db:migrate
```

```ruby:app/models/User.rb
class User < ApplicationRecord
  def self.find_or_create_from_auth(auth)
    provider = auth[:provider]
    uid = auth[:uid]

    self.find_or_create_by(provider: provider, uid: uid) do |user|
      user.name = auth[:info][:name]
    end
  end
end
```

## セッションコントローラー

```bash
% rails g controller sessions
```

```ruby:app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    user = User.find_or_create_from_auth(request.env["omniauth.auth"])
    session[:user_id] = user.uid
    redirect_to request.env["omniauth.origin"] || root_url
  end

  def destroy
    reset_session
    redirect_to root_url, status: :see_other
  end

  def failure
    redirect_to root_url
  end
end
```

ルーティングを追加します。

```ruby:config/routes.rb
get "/auth/:provider/callback", to: "sessions#create"
get "/auth/failure", to: "sessions#failure"
delete "/sign_out", to: "sessions#destroy"
```

## アプリ全体で利用するメソッドの作成

`application_controller.rb`にメソッドを追加します。Deviseのメソッド名と同じようにしてみました。

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  helper_method :current_user, :user_signed_in?

  private

  def authenticate_user!
    redirect_to root_path unless session[:user_id]
  end

  def current_user
    return unless session[:user_id]
    @current_user ||= User.find_by(uid: session[:user_id])
  end

  def user_signed_in?
    !!session[:user_id]
  end
end
```

# 動作確認

ログインとログアウトの状態に応じて表示するボタンを切り替える例です。

```erb:app/views/layouts/application.html.erb
<% if user_signed_in? %>
  <%= link_to "Sign out", "/sign_out",
    data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
<% else %>
  <%= button_to "Sign in", "/auth/twitter2", method: :post,
    data: { turbo: false } %>
<% end %>
```

`before_action`に`authenticate_user!`を指定して、ログインしているときのみ表示する例です。

```ruby:app/controllers/contents_controller.rb
class ContentsController < ApplicationController
  before_action :authenticate_user!
end
```

# おわりに

Rails 7にTwitterログイン機能を搭載することができました。私はこれまでOmniAuthとdeviseを組み合わせた構成にしていたのですが、OmniAuthのみでシンプルな構成もよさそうなので、次に開発するアプリはこの構成でいきたいと思っています。

# 参考

https://zenn.dev/batacon/articles/e9b4a88ede2889

https://blog.tatehitolog.com/entry/2020/02/09/215835