Railsでomniauth-twitterを使ってTwitter認証をする(2018/10)

# 概要
RailsのTwitterアカウントでの連携ログインについて、少しハマったのでメモしつつ、解決方法を提案できたらと思います。
TwitterAPIの仕様が数ヶ月前に変更されたあとの記事が少なく感じました(2018/10現在)。
また、deviseなるものとomniauth-twitterを連携させたものがあるようですが、今回はomniauth-twitter単体での実装です。また、作ったアプリケーションはHerokuにデプロイするところまで書きたいと思います。
# 開発環境
macOS High Sierra v10.13.6
ruby 2.5.1
rails 5.2.1
direnv
Heroku
# 準備
RailsやRubyの導入は省きます。
## direnv
ディレクトリごとに環境変数を設定できるコマンドラインツールです。
### 導入
- macOS  
Homebrewが入っているなら、コマンドひとつでインストールできます。

```bash
$ brew install direnv
```

- Windows・その他
リリースページからダウンロードしましょう。  
https://github.com/direnv/direnv/releases

インストール後は忘れずに「.bashrc」に設定を書きます。

```bash
$ echo 'export EDITOR=vim' >> ~/.bashrc
$ echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
$ source ~/.bashrc
```

詳しい使い方は次のURLを参照してください。
https://www.softantenna.com/wp/review/direnv/


## Heroku
言わずとしれたデプロイサイト。railsアプリをpushするだけでデプロイが終わっちゃう驚き。
ローカル環境でOK!って人は飛ばしてください。
Herokuを導入する以前にgitを導入する必要があるんですが、ここでは省略します。
Herokuは次のURLからダウンロードしてください。
https://devcenter.heroku.com/articles/heroku-cli

## TwitterAPI
個人的にちょっと厄介なこいつ。2018年９月の仕様変更によってAPIの使用に審査が必要になって、使用目的とか取得したデータの扱いとかを３００文字以上の英語で送ります。Google翻訳を駆使したりしてがんばります。
APIの審査についてのURLです。ちなみに私は審査を出した当日に承認されました(2018/10)。
https://qiita.com/tdkn/items/521686c240b0c5bc6207

**【重要】TwitterAPIの設定なんですが、CallBack_URLを適切に指定しないとエラーでます。**
これ、意外とハマるんじゃないかと思います。
omniauth-twitterのみの場合、callback_urlは
http://localhost:3000/auth/twitter/callback
とかですね。
deviseをかませる場合少し違った気がします。
~~どこぞのQiita記事でlocalhostはcallback_urlに指定できないとか書いてたのでそれは間違いだって言いたいです。~~
TwitterAPIのCallBackについて次の記事を参考にしました。
https://saruwakakun.com/memo/omniauth-twitter
# ローカル導入
まずは、ローカルでの構築を行っていきます。
## 新規アプリ作成とGem導入

```bash
$ rails new twitter-auth
$ cd twitter-auth/
```
twitter-authというアプリ名で作成します。次にGemfileを編集します。

```Gemfile
gem 'omniauth'
gem 'omniauth-twitter'
```

Gemfileを編集したので忘れずにbundle installをします。

```bash
$ bundle install
```

## APIキーの登録
用意したTwitterAPIを環境変数に設定します。

```bash
$ direnv edit .
```

vimが開きます。
vimの詳しい使い方についてはコチラ。
とりあえず、「i」キーを押してINSERTモードに変更して

```.envrc
export TWITTER_KEY="your_twitter_api_key"
export TWITTER_SECRET="your_twitter_api_secret"
```

書き終えたら「Esc」キーでINSERTモードを抜けて、
「:wq」と入力します。するとターミナルに戻ります。
環境変数はGitとかで公開してはダメなので、「.gitignore」に追記します。

```bash
$ echo ./.envrc >> .gitignore
```

次に、設定した環境変数をomniauthに設定します。ファイルが無いので新規作成して次を記述します。

```ruby:config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
    provider :twitter,
    ENV["TWITTER_KEY"],
    ENV["TWITTER_SECRET"]
end
```

## Userモデル作成
まずはモデルを作成します。

```
$ rails g model user provider:string uid:string user_name:string image_url:string
```

次にモデルの定義です。引数のauthはTwitterAPIのレスポンスデータで、様々な情報が格納されている。

```ruby:app/model/user.rb
class User < ApplicationRecord
    def self.find_or_create_from_auth(auth)
        provider = auth[:provider]
        uid = auth[:uid]
        user_name = auth[:info][:name]
        image_url = auth[:info][:image]

        self.find_or_create_by(provider: provider, uid: uid) do |user|
            user.user_name = user_name
            user.image_url = image_url
        end
    end
end
```

この他にもデータを取得したい場合は次の一覧から適切な変数を指定することでデータを取得できる。
https://github.com/arunagw/omniauth-twitter#authentication-hash

ex)　ログインした人のTwitterURLをtwitter_urlに格納する。

```ruby
twitter_url = auth[:info][:urls][:Twitter]
```

## Login/Registerコントローラ作成
ログイン、ログアウトに使うSessionコントローラを作成します。

```
$ rails g controller sessions
```

そして次を記述します。

```ruby:app/controller/sessions_controller.rb
class SessionsController < ApplicationController
    def create
        user = User.find_or_create_from_auth(request.env['omniauth.auth'])
        session[:user_id] = user.uid
        flash[:notice] = "ユーザ認証が完了しました。"
        redirect_to root_path
    end

    def destroy
        reset_session
        flash[:notice] = "ログアウトしました。"
        redirect_to root_path
    end
end
```

## ヘルパーメソッドの記述
current_user(ログイン中のユーザ情報)の取得やログイン中かどうかを返すヘルパーメソッドを定義します。

```ruby:app/controller/application_controller.rb
class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
    helper_method :current_user, :logged_in?, :authenticate
  
    private
  
    def current_user
      return unless session[:user_id]
      @current_user ||= User.find_by(uid: session[:user_id])
    end
  
    def logged_in?
      !!session[:user_id]
    end
  
    def authenticate
      return if logged_in?
      redirect_to root_path, alert: "ログインしてください"
    end
end
```

## viewの設定
viewを定義します。今回は、pagesのindexをroot_pathとして、showをログインが必要なページとします。

```
rails g controller Pages index show
```

まずは、全体に適用するviewとして、<body>部分を下に変更してください。

```erb:app/view/layout/application.html.erb
<body>
  <header>
    <% if logged_in? %>
      <%= link_to 'Logout', '/signout' %>
    <% else %>
      <%= link_to 'Twitter Login', '/auth/twitter' %>
    <% end %>
  </header>
  <% if flash[:notice] %>
    <div class="notice">
      <%= flash[:notice]%>
    </div>
  <% end %>
  <%= yield %>
</body>
```

ぶっちゃけ、indexとshowは何も書かなくていいような気がするんですけど、一応わかりやすくするために書いてます。

```erb:app/view/pages/index.html.erb
<h1>TwitterAuthTest</h1>
<p>Find me in app/views/pages/index.html.erb</p>
<%= link_to 'User Page', '/pages/show' %>
```

```erb:app/view/pages/show.html.erb
<% if logged_in? %>
<h1>こんにちは、<%=  current_user.user_name %>さん</h1>
<p>ユーザー用ページです。</p>
<% else %>
<p>ログインしてください</p>
<% end %>
```

## routesの設定
routesの設定です。

```ruby:config/routes.rb
Rails.application.routes.draw do 
  root 'pages#index'
  get 'pages/show'
  get '/auth/:provider/callback', to: 'sessions#create'
  get '/signout', to: 'sessions#destroy'
end
```
## ローカルで動かす
以上で導入は完了です。ローカルで動かすためにはマイグレーションをするのを忘れないでください。

```
$ rails db:migrate
$ rails s
```

# 起動画面
起動画面はこんな感じです。
![スクリーンショット 2018-10-20 4.15.07.png](https://qiita-image-store.s3.amazonaws.com/0/233730/0f7b7d85-9351-3ec4-11ee-76f6bcd9bf45.png)
Twitterでログインをしたら。
![スクリーンショット 2018-10-20 4.15.55.png](https://qiita-image-store.s3.amazonaws.com/0/233730/6a2abf06-aaaa-6302-da09-bda8f9ae3798.png)
User Pageをクリック。
![スクリーンショット 2018-10-20 4.15.37.png](https://qiita-image-store.s3.amazonaws.com/0/233730/8ba4a159-fd23-889b-ad5b-b6bd540d6591.png)
~~(一部隠してます)~~

# Herokuにデプロイ
Herokuにデプロイしてみます。

## データベースシステムの変更
まず、現在使用しているSQLite3はHerokuで使えない仕様なので、PostgreSQLを使うようにGemを変更します。

 - 上の方にある「gem 'sqlite3'」は削除するか、先頭に#をつけてコメントアウトしましょう。
 - 「group :development do」の中に「gem 'sqlite3'」を書きます。
 - 「group :production do」を追記して中に「gem 'pg'」を書きます。(endを忘れずに書きます。)

これは、データベースシステムを開発環境ではSQLite3を使って本番環境　（Heroku)ではPostgreSQLを使いますという意味です。

```Gemfile
.
.
# gem 'sqlite3'
group :development do
  .
  .
  gem 'sqlite3'
end

group :production do
  gem 'pg'
end
.
.
```

今後、ローカルで開発するときのためにオプションをつけてbundle installします。

```
$ bundle install --without production
```

Rails Tutorialでもおなじみのこれです。groupがproductionのものはローカルにインストールしないという意味です。

## いざデプロイ
herokuにデプロイする準備ができたのでデプロイします。

```
$ heroku create　[your_heroku_app_name]
$ git add .
$ git commit -am "init"
$ git push heroku master
$ heroku run rails db:migrate
$ heroku config:set TWITTER_KEY=your_twitter_api_key
$ heroku config:set TWITTER_SECRET=your_twitter_api_secret
```

TwitterAPIのページでCallBack_URLを設定します。これがないとエラー吐きます。
CallBackに以下のリンクを追加します。
https://[your_heroku_app_name].herokuapp.com/auth/twitter/callback

うまくデプロイできれば次のURLでアプリケーションが動いてるところを見ることができます。
https://[your_heroku_app_name].herokuapp.com/

# ハマったポイント
ここから恥ずかしい話になります。
これを実装するのに３日ほどかかりました。なぜかというと、400エラーやら403エラーに襲われたからです・・・。
もしかしたら同じようなエラーで困っている人のために解決法を書いておきます。
## 400エラー
私の場合、TwitterのAPIキーがうまく設定できておらず、APIリクエストを投げる際にAPIキーを送れていなかったときに出ました。
``direnv``の設定で``.bashrc``に``eval "$(direnv hook bash)"``を書き忘れていたおかげでrailsが``.envrc``を読まないという原因でした。

## 403エラー
TwitterAPi設定で、CallBackURLを正しく指定していなかったときに発生しました。
ローカルで開発の場合は、
http://localhost:3000/auth/twitter/callback
http://127.0.0.1:3000/auth/twitter/callback
この２つを設定しておきましょう。
Herokuなどにデプロイした場合も、その都度callback_urlを設定するのを忘れないようにしましょう。

# まとめ
Railsにomniauth-twitterを用いたTwitter連携ログイン機能について軽く説明を交えつつ実装方法を書きました。
Rails初心者が書いた&コピペなので間違ったコードや間違った説明などしている所あるかもしれません。
今回の開発をgithubに上げました。
https://github.com/white0221/twitter_auth_on_rails
最後まで読んでいただきありがとうございました。
開発の助けになれば幸いです。

# 参考
https://github.com/arunagw/omniauth-twitter
https://qiita.com/puremoru0315/items/f1d459b663fd3b715dee
https://qiita.com/ntkgcj/items/c3108c19fb64acc9dd8d