【omniauth+omniauth-twitter】DeviseなしでTwitterユーザー認証を実装する【Rails6】

#はじめに
Twitter認証をDeviseなしで実装しました。
備忘録も兼ねて記事にしたいと思います。

【実行環境】
・ Rails 6.0.0
・ Ruby 2.6.5

【使用gem】
gem 'omniauth'
gem 'omniauth-twitter'
gem 'dotenv-rails'

[公式サイト](https://developer.twitter.com/en/docs/authentication/guides/log-in-with-twitter)


#Twitter API のアカウント取得
まずは開発者用のアカウントを取得します。
[Twitter API 登録](https://qiita.com/kngsym2018/items/2524d21455aac111cdee)
私はこちらをの記事を参考にさせていただきました。
申請の際に英文を書く必要がありますが、私はDeepLで翻訳したものを記入しました。
私の時は2020年9月2日に申請を行いましたが即認証されました。

私の記入例です。使用目的が限られていたのでこの二つを記入しました。
>【TwitterAPIの利用目的】
　私は今、卒業制作用のwebアプリを制作しています。プロフィールを表示し、ユーザーがスケジュールを記入しておけば日程の合う人が可視化され、リプライを送ることができるアプリを制作する予定です。Twitter APIを使用してTwitterログインとフォロワーの取得、リプライを送ることができる機能を実装したいと考えています。

>英訳
I'm currently working on a web app for my graduation project.
I will create an app that displays profiles and allows users to fill out a schedule to visualize and send replies to people who match their schedule.
I would like to implement the ability to use the Twitter API to get Twitter login and followers and send replies.


>【アプリはツイート、リツイート、いいね、フォロー、ダイレクトメッセージ機能を使用しますか？】
アプリでスケジュールの合致した相手を検索し、検索結果に応じてメッセージを送る相手を選択してリプライを送りたいと思っています。検索条件に相互フォローであることを加えたいと思っています。

>英訳
I'd like to search for people who match my schedule on the app and send a reply by selecting the person I want to message based on the search results.
I'd like to add that one of the search criteria is mutual following.

#アプリ作成
アカウント取得後は[こちら](https://developer.twitter.com/en/portal/projects-and-apps)にログインしアプリの設定を行います。
アプリの名前などを登録しますが一番大事なのはcallbackURL(登録後に遷移するページのURL)の設定です。
401エラーがでたら真っ先にここを疑いましょう。

アプリ詳細画面の[Authentication settings]よりeditをクリック。
CALLBACK URLSにローカル環境と本番環境のURLを２つずつ指定します。
ここで指定するURLはTwitter認証後に飛びたいURLになります。
私の場合はルートパスに飛ばしたかったのでこのように記述してあります。

`http://localhost:3000/auth/twitter/callback`
`http://127.0.0.1:3000/auth/twitter/callback`
`http:<本番環境のドメイン>/auth/twitter/callback`
`https:<本番環境のドメイン>/auth/twitter/callback`

私はAWSのEC２をドメイン取得せずに利用していたので
http:12.315.678.910/auth/twitter/callback （本番環境のドメインがなくIPアドレスを記述した例）
このようにIPアドレスを直で入力していました。


#Twitter連携のgemを導入

Twitter連携をするためにomniauthとomniauth-twitterのgemをインストールします。
これらを使うことでTwitterアカウントの情報を使ってユーザー登録やログインなどができるようになります。
この二つについての説明は[こちらの記事](https://codezine.jp/article/detail/10970)が詳しいです

```ruby:Gemfile
gem 'omniauth'
gem 'omniauth-twitter'
```

```
% bundle install
```

#TwitterのAPIキーとTwitterAPIシークレットキーを環境変数として定義する
gemのインストールが終わったらTwitterのAPIキーとTwitterAPIシークレットキーを取得し、環境変数に保存します。
dotenv-railsを導入することでrailsでも簡単に環境変数を扱うことができるようになります。

```ruby:Gemfile
gem 'dotenv-rails'
```
```
% bundle install
```

[アプリ一覧画面](https://developer.twitter.com/en/portal/projects-and-apps)からアプリ詳細画面へと飛び、「Keys and tokens」タブからTwitterAPIキーとTwitterAPIシークレットキーを取得してきます。
これらは公開したくないデータなのでアプリケーションディレクトリのルートディレクトリ(appやdbやGemfileがあるディレクトリ)に「.env」を作成し、環境変数として定義します。


```ruby:.env
TWITTER_API_KEY = '取得してきた自分のTwitterAPIキー'
TWITTER_API_SECRET = '取得してきた自分のTwitterAPIシークレットキー'
```
また、せっかく設置した環境変数をgitに上げないためにも、.gitignoreに記述しpushの対象から外しておきます。記述場所は最下部でいいです。

```ruby:.gitignore
##~省略~
/.env
```
マージしてmasuterブランチにpullした後で、開発環境でログインができなくなった！
という場合には環境変数を定義した.envファイルがmasuterブランチになかったのが原因である場合もあるのでエラーが起きたら疑ってみるのもいいでしょう。私がそうでした。
その場合はmasterブランチでまた同様に.envファイルを作成し環境変数を設置すれば大丈夫です。



#認証に使用するTwitterAPIキーとTwitterAPIシークレットキーの宣言

次に「config/initializers/omniauth.rb」を作成し、下記のように記入します。
説明のため一部の記述だけに留めています。後でまた戻ってきます。

```ruby:config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET']
end
```

2行目でTwitter認証に使う、環境変数に定義したTwitterAPIキーとTwitterAPIシークレットキーを宣言しています。


#モデルとデータベースの作成
次にuserモデルとテーブルを作成します。
指定しているカラムはTwitterのID、アカウント名（リプライで使う＠の後ろにつく英数字）、ユーザー名、画像パスを保存するためのものです。
他のSNS認証やDeviseと併用する場合はproviderカラムも作成する必要があるそうですが、Twitter認証のみなのでここでは作成しません。
これらは全てomniauth-twitterで取得できる値です。その他の値など詳しくは[omniauth-twitterのREADME](https://github.com/arunagw/omniauth-twitter#authentication-hash)をどうぞ。

```
% rails g model user uid:string nickname:string name:string image:string
% rails db:migrate
```

```ruby:user.rb
class User < ApplicationRecord
  def self.find_or_create_from_auth_hash(auth_hash)
    uid = auth_hash[:uid]
    nickname = auth_hash[:info][:nickname]
    name = auth_hash[:info][:name]
    image = auth_hash[:info][:image]
    # find_or_create_by()は()の中の条件のものが見つければ取得し、なければ新しく作成するというメソッド
    find_or_create_by(uid: uid) do |user|
      user.uid = uid
      user.nickname = nickname
      user.name = name
      user.image = image
    end
  end
end
```

#コントローラーの作成

```
% rails g controller Sessions
```

#セッション管理用のヘルパーメソッド作成
deviseを使用していないためログイン、ログアウトなどのメソッドを自作する必要があります。


```ruby:app/helpers/sessions_helper.rb
module SessionsHelper
  # 渡されたユーザーでログインする
  def log_in(user)
    session[:uid] = user.uid
  end

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    @current_user ||= User.find_by(uid: session[:uid]) if session[:uid]
  end

  # 受け取ったユーザーがログイン中のユーザーと一致すればtrueを返す
  def current_user?(user)
    user == current_user
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end

  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:uid)
    @current_user = nil
  end
end

```
作成したヘルパーメソッドを全てのページで使えるようにするために、Applicationコントローラから読み込みを行います。

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include SessionsHelper
end
```



#ログイン、ログアウト、認証に失敗した時のアクションを記述。


```ruby:sessions_contoroller
class SessionsController < ApplicationController
  def create
    raise 'request.env[omniauth.auth]がありません' if auth_params.nil?

    user = User.find_or_create_from_auth_hash(auth_params)
    if user
      log_in(user)
      flash[:notice] = 'ログインしました'
      redirect_to root_path
    else
      flash[:notice] = '失敗しました'
      redirect_to root_path
    end
  end

  def destroy
    log_out if logged_in?
    flash[:notice] = 'ログアウトしました'
    redirect_to root_path
  end

  # callbackに失敗したときに呼ばれるアクション
  def failure
    flash[:notice] = 'キャンセルしました'
    redirect_to root_path
  end

  private
  # request.env['omniauth.auth']はTwitter認証で得た情報を格納するもの
  def auth_params
    request.env['omniauth.auth']
  end
end
```

#認証に失敗した時のコールバックの記述

```ruby:config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV['TWITTER_API_KEY'], ENV['TWITTER_API_SECRET'], callback_url: ENV['TWITTER_API_SECRET'] ##コールバックURL明示
  OmniAuth.config.on_failure = Proc.new { |env|
  OmniAuth::FailureEndpoint.new(env).redirect_to_failure ##コールバックに失敗した時のアクション設定
}
end
```
先ほど記述したファイルに付け足しを行なっています。
callbackが失敗した時、どこにリダイレクトすればいいのかはconfig/initializers/omniauth.rbで設定できます。
callback_url: ENV['TWITTER_API_SECRET']はコールバックURLを明示化するための記述です。
ローカル環境では認証できていたのに、本番環境ではコールバックが上手くいかなかったということのないように記述しています。

開発環境と本番環境で指定するコールバックURLが異なるため、URL部分は環境変数で代入しています。

```ruby:.env（開発環境）
TWITTER_API_KEY = '自分のTwitterAPIキー'
TWITTER_API_SECRET = '自分のTwitterAPIシークレットキー'
TWITTER_CALLBACK: 'http://localhost:3000/auth/twitter/callback' ##開発環境に設置する
```


```ruby:(本番環境)
TWITTER_API_KEY = '自分のTwitterAPIキー'
TWITTER_API_SECRET = '自分のTwitterAPIシークレットキー'
TWITTER_CALLBACK=`http:<本番環境のドメイン>/auth/twitter/callback` ##本番環境に設置する
```

デプロイできたら本番環境側にも環境変数を設置する必要があります。
例えば本番環境でHerokuを使う場合はこのようにして設置します。

```
% heroku config:add TWITTER_API_KEY = '自分のTwitterAPIキー'
% heroku config:add TWITTER_API_SECRET = '自分のTwitterAPIシークレットキー'
% heroku config:add TWITTER_CALLBACK=`http:<本番環境のドメイン>/auth/twitter/callback`
```



#ビューをログインとログアウトで表示を変える
ログインとログアウト用のリンクも記述しています。

```erb:app/views/layouts/_header.html.erb（お好みのページ）
<header>
.
.
 <% if logged_in? %>
  <%= link_to 'ログアウト', logout_path, class: "logout" %>
 <% else %>
  <%= link_to 'ログイン', "/auth/twitter", class: "login" %>
 <% end %>
.
.
</header>
```

#ログイン状況の確認

ログインしていないユーザーをトップページへリダイレクトさせるアクションです。
application_controllerに追加しているので全てのコントローラーで利用できます。

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include SessionsHelper

  private

  # ログイン済みユーザーかどうか確認　
  def logged_in_user
    redirect_to root_path unless logged_in?
  end
end
```

またbefore_actionを使用すると、コントローラで定義されたアクションが実行される前に、共通の処理を行うことができるようになります。

```ruby:例
class 好みのコントローラ名 < ApplicationController
  before_action :処理させたいメソッド名
  before_action :logged_in_user, only:[:edit, :update]
```




#ルーティングを設定
最後に各ルーティングを設定して終わりです。

```ruby:config/routes.rb
get '/auth/:provider/callback', to: 'sessions#create' #ログイン認証
get '/logout', to: 'sessions#destroy' #ログアウト用
get "/auth/failure", to: "sessions#failure" #認証失敗時用
```




#終わり
自分が次また同じように設定する時、忘れないように書きました。
間違いや改善点などあれば言ってもらえればと思います。

外部APIを使用しているのでTwitter認証の結合テストコードを書くときにもすごい苦労したのですがそれもまた余裕のあるときに書きたいと思います。
[結合テストコードを記述した時の記事を書きました](https://qiita.com/guguasmon/items/d903e2cc43dead84e00f)

参考にさせてもらった記事
https://qiita.com/d0ne1s/items/7a96b835280f55bb5234
https://qiita.com/keiya01/items/c96a0393c76f5560ee41
https://qiita.com/d0ne1s/items/7c4d2be3f53e34a9dec7
https://qiita.com/ngron/items/88f04a2d864f9f33b389
https://www.wantedly.com/companies/clueit/post_articles/43708