【HowTo】某フリマサイトにRails+Devise+Twitter APIを利用した新規登録とログイン機能を実装してみた。

某スクールのチーム開発にて某フリマアプリを作成しており、そちらのオリジナル機能として、TwitterのAPIとDeviseを利用した新規登録とログイン機能を実装しました。

TwitterのAPIの使用方法などに関して、以下に概要と手順をまとめます！
今回のtwitterの認証、この後の別機能追加につなげますので、そちらは完成次第、また記事にして投稿します！
皆様の実装のご参考になれば、幸いです！
*APIを設定するために様々な設定が必要です。長文で恐縮ですが、最後まで確認いただけると幸いです。
(Deviseの実装・Css等細かいviewについては、記述が長くなるため、今回は割愛いたします。）

本記事該当Github：　https://github.com/Tatsu88-Tokyo/freemarket_sample_60ce

#TwitterのAPI申請
まず、TwitterのAPIを使用するにあたって、Twitterのdeveloperサイトから申請を行う必要があります。
https://developer.twitter.com/en.html

サイト内、[Get start]=>[Create an app]から登録をしましょう！
利用申請にあたって、英語でAppの説明文などの記述が必要となります。
申請内容の記述が終わったら、問題ない場合、メールが届きます。
(私の場合は、すぐに承認され、メールが来ました)

申請内容に関しては、以下のURLで簡潔に説明されていたので、以下をご参照ください！
https://digitalnavi.net/internet/3072/
*以下、参照の項目でもいくつかURLを記載してますので、こちらも必要に応じてご確認ください。

#動作イメージ
##新規登録
それでは、今回の実装内容のイメージを以下に貼り付けていきます。
新規登録時、以下のページから移動します。
![demo](https://gyazo.com/c8fd4f50246f187e397a1f81327f0c1d/raw)

メールアドレスからの登録時は、以下のビューに飛びます。
ニックネーム・アドレス・パスワードをご自身で入力いただきます。
![demo](https://gyazo.com/b6ec9d313dbaa2cdd3ad6d237e951fc8/raw)

今回のTwitterで新規登録をすると以下のビューにとび、Twitterのニックネーム・アドレスが自動で入力され、パスワードの入力は不要となります。＊アドレスはTwitter上、設定していないと出てきません。
![demo](https://gyazo.com/f27d3734ab035c02526cbea5d31c401d/raw)


##ログイン
一度新規登録ができると、ログインは以下のように動作します。

![demo](https://gyazo.com/053617ad4de0b7f33dd2d9b2aaaa0856/raw)

#実装ポイント
- TwitterのAPIを利用した場合の新規登録はニックネームとアドレスが自動入力される。
- さらにパスワードの入力を省略する。
- ログイン時はAPI情報を取得することで、アドレスなどの入力を省略する。

それでは、実装の仕方などを以下に記載していきます。

#事前準備
##Gemのインストール
まず、TwitterのAPIに対してアクセスを行うために便利なGemである'omniauth-twitter'がありますので、そちらを入れましょう。

```rb:Gemfile
gem 'omniauth-twitter'
gem "omniauth-rails_csrf_protection"
```

```terminal:ターミナル
$ bundle install 
```
* "Omniauth-twitter"のREADMEは以下になります。ご確認ください。
https://github.com/arunagw/omniauth-twitter

* "omniauth-rails_csrf_protection"は、omniauthの脆弱性を解消するために入れます。
GETメソッドでCSRF tokenを送るよりも、POSTメソッドの方が通信の信頼性が高いため、自動的にPostメソッドに変換してくれます。
詳細は以下、ご参照ください。

- 脆弱性について
https://github.com/omniauth/omniauth/pull/809
- gem "omniauth-rails_csrf_protection"について
https://github.com/cookpad/omniauth-rails_csrf_protection

##環境変数の設定
次は、Twitterのアプリ登録時に取得した鍵を環境変数に格納しましょう。

* 今回はRailsのver上、secrets.ymlを使用してます。
* またMac OSがCatalina以降の方は".bash_profile"ではなく、".zshrc"になるので注意しましょう。

```terminal:ターミナル
$ vim ~/.bash_profile

#まず「i」を押して入力モードにしましょう
export TWITTER_CLIENT_ID='TwitterのAPIキーを入れましょう'
export
TWITTER_CLIENT_SECRET='TwitterのAPI　SECRETキーを入れましょう'
#入力後、"escキー"=> ":" => "w" => "q"の順でコマンドを打ちましょう。

#鍵を記述後、保存を行います。
$ source ~/.bash_profile
```

```yaml:secrets.yml
#development/productionに以下の記述をしましょう。
twitter_client_id: <%= ENV["TWITTER_CLIENT_ID"] %>
twitter_client_secret: <%= ENV["TWITTER_CLIENT_SECRET"] %>
```

##環境変数の読み込み(devise)
今回はdeviseを使用しますので、以下に環境変数を読み込みます。

```rb:config/initializers/devise.rb
config.omniauth :twitter,ENV['TWITTER_CLIENT_ID'],ENV['TWITTER_CLIENT_SECRET']
```

##SNS認証に必要なモデルの準備
今回、「SNS認証とユーザー登録のタイミングが異なる」仕様であり、SNS認証時にはusersテーブルのレコードを作成することはできません。
そこで、SNSに関わる別テーブルを用意します。
今回は、SNS認証時の情報を保存するSnsCredentialモデルを作成し、そこにuidやproviderを保存します。
さらに、Userモデルとのアソシエーションのために、外部キーとしてuser_idを持たせておきます。

```terminal:ターミナル
$ rails g model sns_credential provider:string uid:string user:references
$ rails db:migrate
```

```rb:migrion_file
class CreateSnsCredentials < ActiveRecord::Migration[5.0]
  def change
    create_table :sns_credentials do |t|
      t.string :provider
      t.string :uid
      t.references :user, foreign_key: true

      t.timestamps
    end
  end
end
```

```rb:models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
#omniauthableとomniauth_providersが追加オプションです。
         :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:twitter]
  has_many :sns_credentials
```

```rb:models/sns_credential.rb
class SnsCredential < ApplicationRecord
  belongs_to :user, optional: true
end
```

##deviseのコントローラ準備
続いてdeviseのコントローラをカスタマイズするためのコントローラを作成します。

```terminal:ターミナル
$ rails g devise:controllers users
```
上記にて、controllers/users以下にdeviseのクラスを継承したコントローラが生成されます。
このコントローラ内で、deviseが提供するアクションの内容をカスタマイズすることができます。

まず、このコントローラを使用させるためにdeviseのルーティングを変更します

```rb:config/routes.rb
# 変更前
devise_for :users

# 変更後
devise_for :users, controllers: {
  omniauth_callbacks: 'users/omniauth_callbacks',
  registrations: 'users/registrations'
}
```

#コントローラ/モデルの編集
ここまででSNS認証を実装する準備が整いましたので、これからコントローラとモデルの記述を行います。
ユーザーとシステムの動きは以下の通りです。
1. 認証のボタンがクリックされる
2. WebAPI側にリクエストが送られる
3. 認証を経て、omniauth_callbacksコントローラにSNSに登録されている情報が返される
4. SNSの情報から、ユーザー情報のみを取得して、既存のユーザー情報と照合を行う
5. その照合結果から、今回SNSで認証されたユーザーが、すでにアプリケーションに登録されているユーザーなのか判断する
6. 照合の結果、既存のユーザーが存在しない場合は、新規登録画面に遷移する
7. すでに同じ情報のユーザーがアプリケーションのDBに存在している場合は、ログイン処理を行う

それでは、早速編集していきましょう。
##コントローラの編集
ポイント

- APIから受け取ったレスポンスがrequest.env["omniauth.auth"]という変数に入っており、その情報を”sns_info”とする。
*request.env['omniauth.auth']では、ユーザー名など様々な情報が取得できます。
詳細はOmniauth-twitterのREADMEをご参照ください。
- DB操作を行うメソッドUser.from_omniauthを作り、変数”sns_info”を渡す。
- このメソッドには＠userへ代入することで、その後の処理でdeviseのヘルパーを利用することができる。
- ＠userが未登録の場合(新規登録する場合)、＠userという変数をそのまま新規登録のviewsで利用するためにrenderを使用する。

```rb:controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

 def twitter
    authorization
  end

  def failure
    redirect_to root_path
  end

  private

  def authorization
  #env["omniauth.auth"]からユーザーの情報を取得します。

    sns_info = User.from_omniauth(request.env["omniauth.auth"])
    @user = sns_info[:user]

  #ユーザー情報が登録済みの場合、ログイン処理を行う
    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
    else
  #ユーザー情報が未登録の場合、deviseの新規登録に移動する。
      @sns_id = sns_info[:sns].id
      render template: 'devise/registrations/new'
    end
  end
```


##モデルの編集
続いてモデルの編集を行います。
上記で作成したモデルファイルに対して、以下の記述を追加します。
ポイント

- 過去にSNS認証をしている人、もしくは同じアドレスでユーザ登録している人はログインに移行するため、sns_idを保存します。
- まだ登録を行っていない人はニックネームとアドレスを取得する

*first_or_createメソッド：
任意のレコードを新規保存する際に、既に同じ情報を持ったレコードがある場合被らせたくないときに使用します。
whereメソッドと併用することで、検索条件に合致するレコードが存在する場合にはそのレコードを参照し、無ければ検索条件の内容で新しいレコードを新規保存してくれます。
返り値は参照したレコードもしくは新規作成したレコードです。

```rb:models/user.rb
def self.from_omniauth(auth)
  sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
  # sns認証したことがあるかemailでユーザー検索して取得orビルド(保存はしない)
  user = sns.user || User.where(email: auth.info.email).first_or_initialize(
    nickname: auth.info.name,
    email: auth.info.email
  )
  # userが登録済みの場合はそのままログインの処理へ行くので、ここでsnsのuser_idを更新しておく
  if user.persisted?
    sns.user = user
    sns.save
  end
  user
end
```

#SNS認証時にはパスワード入力を不要にする
SNS認証をするため、ユーザの負担を軽くするために、パスワードを自動生成するという方針で実装します。
##モデル編集
変更点

- userとsnsをハッシュで返すようにします。（最後の行）

```rb:models/user.rb
# 変更前
  def self.from_omniauth(auth)
    sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
    user = sns.user || User.where(email: auth.info.email).first_or_initialize(
      nickname: auth.info.name,
      email: auth.info.email
    )
    if user.persisted?
      sns.user = user
      sns.save
    end
      user
  end

# 変更後
  def self.from_omniauth(auth)
    sns = SnsCredential.where(provider: auth.provider, uid: auth.uid).first_or_create
    user = sns.user || User.where(email: auth.info.email).first_or_initialize(
      nickname: auth.info.name,
      email: auth.info.email
    )
    if user.persisted?
      sns.user = user
      sns.save
    end
    { user: user, sns: sns }
  end
```

上記の変更で、モデルから返される値が変わったので、コントローラでも代入する部分を修正していきます。
##コントローラ編集
変更点

- ＠userへ代入する際にハッシュから値を取り出す
- ＠sns_idという変数を作り、新規登録画面で扱えるようにする

```rb:controllers/users/omniauth_callbacks_controller.rb
# 変更前
    @user = User.from_omniauth(request.env["omniauth.auth"])
    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication 
    else
      render template: 'devise/registrations/new'
    end

# 変更後
    sns_info = User.from_omniauth(request.env["omniauth.auth"])
    @user = sns_info[:user]

    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication
    else
      @sns_id = sns_info[:sns].id
      render template: 'devise/registrations/new'
    end
```

続いて、この変数を使ってビューのパスワード入力欄の表示/非表示を分けていきます。
ポイント

- @sns_idが存在すればhidden_fieldでparams[:sns_auth]としてtrueという値を送信します。
- @sns_idが存在しなければパスワード入力欄を表示します。

```rb:views/devise/registrations/new.html.haml
  - if @sns_id.present?
    = hidden_field_tag :sns_auth, true
  - else
    .field
      = f.label :password
      - if @minimum_password_length
        %em
          (#{@minimum_password_length} characters minimum)
      %br/
      = f.password_field :password, autocomplete: "new-password"
    .field
      = f.label :password_confirmation
      %br/
      = f.password_field :password_confirmation, autocomplete: "new-password"
```

このparamsは通常devise/registrations#createアクションへ送信されますが、今回はこのコントローラをオーバーライドするためにcontrollers/users/registrations_controller.rbを作成してあります。
このコントローラ内でcreateアクションの動作を変更しましょう。

*Devise.friendly_tokenメソッド：
Deviseのメソッドで自動的・ランダムにtokenを作成してくれます。
デフォルトでは20文字で生成されます。
https://www.rubydoc.info/github/plataformatec/devise/Devise.friendly_token

*rubyでは継承先のクラスでsuperメソッドを使うことで親モデルの同名メソッドをそのまま実行します。ここではdevise本来のregistrations#createアクションが呼ばれるということになります。

```rb:controllers/users/registrations_controller.rb
  def create
    if params[:sns_auth] == 'true'
      pass = Devise.friendly_token
      params[:user][:password] = pass
      params[:user][:password_confirmation] = pass
    end
    super
  end
```

#viewでリンクの設置
上記にて、設定は完了しましたので、リンクの設置をしましょう。
rails routeでpathを確認し、以下のように設定します。

```haml:route.rb
#新規登録ボタン
=link_to user_twitter_omniauth_authorize_path, method: :post, class: "btn-def--twitter" do
 %i.fab.fa-twitter
 %span<>
 Twitterで登録する

#ログインボタン
= link_to user_twitter_omniauth_authorize_path, method: :post, class: "btn-def--twitter" do
 %i.fab.fa-twitter
 %span<>
 Twitterでログイン 
```

実装自体は上記で完了となりますが、以下、注意点を念のため、確認ください！

#注意点：Callback URLについて
今回のTwitter APIを実装するうえで、最も重要な要素の一つに”Callback URL”の設定がございます。
こちらに関して、設定を誤るとエラーが出てしまいますので、しっかりとチェックして設定しましょう。
手順
1.　まず、Rails routeをしましょう。
2.　その中にあるtwitter callbackのURLを確認しましょう。
3.　twitter developers siteに移動し、自身のAppページに行きましょう
4.　そこのApp detailからcallback urlを記載する箇所に先ほど確認したURLを記載しましょう。
*私の場合は、”http://localhost:3000/users/auth/twitter/callback”と設定しました。
*さらに本番環境でも使用する場合は、上記の”http://localhost:3000”の部分をご自身の本番環境URLに書き換えて設定しましょう。例(http://本番環境URL/users/auth/twitter/callback) 
5.　変更内容をSaveしましょう。

以上で設定できます。
ここに誤ったURLを指定しているとエラー原因となります。
エラー発生時はよく確認しましょう。(私はここで詰まりました笑)　

#注意点
##website URLの設定について
twitterのAPI申請時、website URLの記載が必要です。
しかしながら、こちらのURL、ローカル環境やドメインが無い場合は登録できません。
私の場合、今回、ドメインを取得していないので、仮のURLで設定しました。
*TwitterのマイページのURLやQiitaのマイページなど、他の人と被らないURLを仮で設定しましょう。

##メールアドレスを取得するための設定とwebsite URLの設定について
twitterのAPIからメールアドレスを取得するためには、以下のステップが必要です。
1. Privacy Policy URLとTerms of Service URLの登録
2. Permissionでemailの取得をリクエストする

それでは、詳細を以下に記載します。
###1. Privacy Policy URLとTerms of Service URLの登録
登録しているAppにてPrivacy Policy URLとTerms of Service URLの登録が必要となります。
実際にPrivacy Policy URLとTerms of Service URLのページがある方はそのページのURLを記載頂ければ、問題ございませんが、私のように開発環境でしか使用しない方は、そのようなURLを持っていない可能性あると思います。
そのような場合はwebsite URLと同じURLでも問題ないので、同様のURLを記載しましょう。

###2. Permissionでemailの取得をリクエストする
上記にてPrivacy Policy URLとTerms of Service URLの登録をした後、自身のAppからPermissionに移動しましょう。
そこの下部に以下の記載がございますので、チェックボックスにチェックを入れましょう。
>Additional permissions
>These additional permissions require that you provide URLs to your application or service's >privacy policy and terms of service. You can configure these fields in your Application Settings.

以上でemailを取得できます！実際に動作を確認しましょう！

#参照
Twitter Developers site
https://developer.twitter.com/en.html

Omniauth-twitter
https://github.com/arunagw/omniauth-twitter


Twitter Developerの開発者申請（例文あり）とAPIキー取得方法まとめ【2019年版】
https://digitalnavi.net/internet/3072/

omniauthの脆弱性について
https://github.com/omniauth/omniauth/pull/809

gem "omniauth-rails_csrf_protection"
https://github.com/cookpad/omniauth-rails_csrf_protection

Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ　※2019年8月時点の情報
https://qiita.com/kngsym2018/items/2524d21455aac111cdee

[Rails] Facebook/Twitter/Googleでのユーザー登録をDevise & Omniauthを使って爆速で実装する
https://qiita.com/kazuooooo/items/47e7d426cbb33355590e

【Rails】deviseのTwitter認証で「Unauthorized 403 Forbidden」が出てしまう場合の対処法
https://saruwakakun.com/memo/omniauth-twitter

OAuth::Unauthorized 401 Unauthorizedなんてエラーがでたら
https://qiita.com/hirokishirai/items/5a43977a38ecd922bfb9

twitterアカウントでログイン devise+omniauth (rails5)
https://qiita.com/ntkgcj/items/c3108c19fb64acc9dd8d

omniauth-twitterでemail情報を取得する
https://qiita.com/shitake/items/fd592bce7d0be8fadd47

覚えておくと幸せになれるActiveRecord::Relationメソッド6選
https://qiita.com/Yama-to/items/4696e9d43ebec6012129

以上となります。最後までご覧いただき、ありがとうございました！
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします！
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。