Railsアプリに簡単なTwitterログインを実装する

簡単なTwitterログインを実装したので、方法をまとめていきます。
ログイン方法はTwitter連携だけなので、devise等は利用していません。

仕様は以下の通りです。

- ログイン方法はTwitterのみ(メールアドレスでのログインは出来ない）
- ログインボタンをクリックすると、Twitterの認証画面に遷移する
- 初めてログインする場合にはusersテーブルにレコードを作成、2回目以降の場合には既存のデータを読み込む
- ログイン後は、sessionが開始する

アプリはすでにあって、トップページ等の設定は出来ている前提で話を進めていきます。

##API Key、API Secret Keyを取得
Twitterと連携するためには、TwitterのAPIキーとAPIシークレットキーが必要です。以下の手順で取得しましょう。

###アカウント取得
まずは以下の記事等を参考に、開発者アカウントを取得しましょう。
[Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ　※2019年8月時点の情報](https://qiita.com/kngsym2018/items/2524d21455aac111cdee)
2019年12月29日に申請を出したところ、即時承認されました。待ち時間0秒でした。

###アプリ作成
アカウント取得、ログイン後
https://developer.twitter.com/en/apps/create
よりアプリを作成します。

**Callback URLs**には
`http:localhost:3000/auth/twitter/callback`
`http:0.0.0.0:3000/auth/twitter/callback`
`http:<本番環境のドメイン>/auth/twitter/callback`
`https:<本番環境のドメイン>/auth/twitter/callback`
の4つを指定します。
最低2つ設定されていないと動かない仕様になっているようです。

他は基本的にはそれっぽいことを書いておけばOKです。
また設定は後から自由に変更できます。

###キーを取得
アプリ一覧画面
https://developer.twitter.com/en/apps
から作成したアプリ右の「Details」ボタンをクリック→「Keys and tokens」タブからAPIキーとAPIシークレットキーが取得出来ます。

##必要なgemのセットアップ
gemのインストールと、各種設定を行います。

###dotenv-rails
APIキーとAPIシークレットキーを環境変数に保存するために`dotenv-rails`を導入します。

```rb:Gemfile
gem 'dotenv-rails'
```
```terminal
$ bundle install
```
プロジェクトルートに`.env`ファイルを作成し、上記で取得した各種キーを書き込みます。

```.env
TWITTER_API_KEY = hogehoge
TWITTER_API_SECRET = foobarbaz
```
###omniauth-twitter
Twitter連携のためのgem`omniauth-twitter`を導入します。

```rb:Gemfile
gem 'omniauth-twitter'
```
```terminal
$ bundle install
```
`config/initializers/omniauth.rb`を作成し、APIキーとAPIシークレットキーを書き込みます。

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

##モデル、データベースを作成
TwitterのID、ニックネーム、ユーザー名、画像パスを保存するためのデータベースと関連するモデルを作成します。
上記は全てomniauth-twitterで取得できる値です。その他の値については[omniauth-twitterのREADME](https://github.com/arunagw/omniauth-twitter#authentication-hash)をご覧ください。

```terminal
$ rails g model User uid:string nickname:string name:string image:string
$ rails db:migrate
```

##セッション管理用のヘルパーメソッドを作成
```app/helpers/sessions_helper.rb
module SessionsHelper
  # 渡されたユーザーでログインする
  def log_in(user)
    session[:uid] = user.uid
  end

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

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

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

##ログイン、ログアウト処理を実装
```terminal
$ rails g controller Sessions
```
```app/controllers/sessions_controllers.rb
class SessionsController < ApplicationController
  def create
    unless request.env['omniauth.auth'][:uid]
      flash[:danger] = '連携に失敗しました'
      redirect_to root_url
    end
    user_data = request.env['omniauth.auth']
    user = User.find_by(uid: user_data[:uid])
    if user
      log_in user
      flash[:success] = 'ログインしました'
      redirect_to root_url
    else
      new_user = User.new(
        uid: user_data[:uid],
        nickname: user_data[:info][:nickname],
        name: user_data[:info][:name],
        image: user_data[:info][:image],
      )
      if new_user.save
        log_in new_user
        flash[:success] = 'ユーザー登録成功'
      else
        flash[:danger] = '予期せぬエラーが発生しました'
      end
      redirect_to root_url
    end
  end


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

##ログイン、ログアウト用のリンクを追加
```erb:app/views/layouts/_header.html.erb
<header class="header">
.
.
  <% if logged_in? %>
    <%= link_to 'ログアウト', logout_path %>
  <% else %>
    <a href="/auth/twitter">ログイン</a>
  <% end %>
.
.
</header>
```

##ルーティングを設定
```config/routes.rb
Rails.application.routes.draw do
.
.
  get '/auth/:provider/callback', to: 'sessions#create'
  get '/logout', to: 'sessions#destroy'
end
```
`omniauth-twitter`の仕様で、`/auth/twitter/callback`が`/auth/:provider/callback`に変換されます。

## ログイン状態の振る舞いのテスト
今回、`session[:uid]`にユーザーidを代入形でログイン処理を実装しました。
RSpecでログイン状態のユーザーを作る時にも`session[:uid] = user.uid`と書きたいところですが、
RSpecのfeature spec及びsystem specではデフォルトで`session`メソッドが使えません。
そこで[rack_session_access](https://github.com/railsware/rack_session_access)をインストールして使えるようにします。

```rb:Gemfile
group :test do
# 省略
  gem 'rack_session_access'
end
```
```zsh
$ bundle install
```
```config/environments/test.rb
Rails.application.configure do
# 省略
  config.middleware.use RackSessionAccess::Middleware
```
```spec/spec_helper.rb
RSpec.configure do |config|
# 省略
  require 'rack_session_access/capybara'
end
```
```spec/systems/sample_spec.rb
it 'ログイン' do
  visit root_path
  expect(page).to_not have_content 'マイページ' # ログイン前はマイページという表示が無い
  page.set_rack_session(uid: user.uid) # ログイン
  visit root_path
  expect(page).to have_content 'マイページ' # マイページと表示されている
end
```
##参考
- [Ruby on RailsでTwitter認証機能を実装してみる](https://reasonable-code.com/rails-twitter-auth/)
- [RSpecのfeatureテストでsessionを扱う方法 - Qiita](https://qiita.com/Yuki_Nakagami/items/85e96075f943ebd9c259)