【Rails】Sorcery & Twitter認証で詰まった話

# はじめに
こんにちは、たけむーです。

今回は、ポートフォリを開発していてtwitterログインの実装で詰まったのでメモとして残しておきます。

アプリ側の設定はSorceryの[wiki](https://github.com/Sorcery/sorcery/wiki/External)と[この記事](https://blog.aiandrox.com/posts/tech/2020/03/29/)を参考にしました。
Twitter Developerの設定は[この記事](https://blog.palettecms.jp/article/20103)を参考にしました。

この記事では設定の説明は省略させもらいます。

# 結論
先に結論を言うと、[Twitter Developer](https://developer.twitter.com/en/portal/dashboard)でアクセス権限を**Essential**から**Elevated**にしましょうという話です。

# 発生したエラー
上記の記事等を参考に設定を行い、実際に認証できるか動かしてみました。
すると、以下のようなエラーが出ました。
```
ActiveRecord::NotNullViolation - PG::NotNullViolation: ERROR:  null value in column "name" of relation "users" violates not-null constraint
DETAIL:  Failing row contains (55, null, null, null, null, 18668cfb-c19d-4d9b-85f3-28ac2719e9a9, 0, 2022-03-19 04:36:58.732046, 2022-03-19 04:36:58.732046, null, null, null, null, 0).:
  app/controllers/oauths_controller.rb:26:in `create_user_from'
  app/controllers/oauths_controller.rb:14:in `callback'
```
パッと見た感じだと、「`Users`テーブルの`name`カラムには**NOT NULL**制約を付与しているのに、**NULL**で保存しようとして制約に引っ掛かってるよ」みたいなエラーだと思います。

確かに今回は`Users`テーブルの`name`カラムに**NOT NULL**制約を付与しています。
```schema.rb
  create_table "users", force: :cascade do |t|
    t.string "name", null: false
    t.string "email", null: false
    t.string "crypted_password"
    t.string "salt"
    t.string "uuid", null: false
    t.integer "role", default: 0, null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end
```

エラーが発生している箇所は`oauths`コントローラの`create_from`の部分です。
```ruby
class OauthsController < ApplicationController
  skip_before_action :require_login

  def oauth
    login_at(auth_params[:provider])
  end

  def callback
    provider = auth_params[:provider]
    if auth_params[:denied].present?
      redirect_to boards_path, success: t('.success', item: provider.titleize)
      return
    end
    create_user_from(provider) unless (@user = login_from(provider))
    redirect_to foods_path, success: t('.success', item: provider.titleize)
  end

  private

  def auth_params
    params.permit(:code, :provider, :denied)
  end

  def create_user_from(provider)
    @user = create_from(provider) # ←ここ
    reset_session
    auto_login(@user)
  end
end
```
この`create_from`でtwitterの情報を使ってユーザーを作成しますが、今回はここで失敗しています。

この時点で、twitterの情報がうまく取得できていないのかなと考えることができます。

# 試したこと（意味なかった）
- API keyやAPI Secret keyが設定できているかの確認、念のため再発行した
- CallbackURLがTwitter Developer側とアプリ側で一致しているか確認
- 開発環境で`http://127.0.0.1:3000/`にアクセスしているか

他にも今回の認証で触ったファイルでタイポ等がないか何回も確認しましたが、そういったミスは見つかりませんでした。

# 解決法
色々見たけど、特に原因はわからず...

ふと思った、そもそも`create_from`って何してるの？
sorceryの定義されている部分を見てみます。
[ここ](https://github.com/Sorcery/sorcery/blob/7268135de1d341d42b1dea80ab6be5f9635a45a2/lib/sorcery/controller/submodules/external.rb#L189)で定義されてました。
以下がメソッドの部分です。
```ruby
def create_from(provider_name, &block)
  sorcery_fetch_user_hash provider_name
  # config = user_class.sorcery_config # TODO: Unused, remove?

  attrs = user_attrs(@provider.user_info_mapping, @user_hash)
  @user = user_class.create_from_provider(provider_name, @user_hash[:uid], attrs, &block)
end
```
一度、create_fromの前で処理を止めて一つずつ見てみましょう。
```bash
    24: def create_user_from(provider)
    25:   binding.pry
 => 26:   @user = create_from(provider)
    27:   reset_session
    28:   auto_login(@user)
    29: end

[1] pry(#<OauthsController>)> provider
=> "twitter"
[2] pry(#<OauthsController>)> sorcery_fetch_user_hash(provider)
=> nil
```
次に、`sorcery_fetch_user_hash`の部分を見てみます。
```ruby
def sorcery_fetch_user_hash(provider_name)
  # the application should never ask for user hashes from two different providers
  # on the same request.  But if they do, we should be ready: on the second request,
  # clear out the instance variables if the provider is different
  provider = sorcery_get_provider provider_name
  if @provider.nil? || @provider != provider
    @provider = provider
    @access_token = nil
    @user_hash = nil
  end

  # delegate to the provider for the access token and the user hash.
  # cache them in instance variables.
  @access_token ||= @provider.process_callback(params, session) # sends request to oauth agent to get the token
  @user_hash ||= @provider.get_user_hash(@access_token) # uses the token to send another request to the oauth agent requesting user info
  nil
end
```
ひとつずつ見ていきました。
（特に問題なかったので省略させていただきます。）
```bash
[4] pry(#<OauthsController>)> sorcery_get_provider provider
省略
[5] pry(#<OauthsController>)> @provider
省略
[6] pry(#<OauthsController>)> @provider != provider
省略
[7] pry(#<OauthsController>)> @access_token
省略
```
問題は次です。
```bash
[8] pry(#<OauthsController>)> @user_hash
=> {:token=>*******************************,
 :user_info=>
  {"errors"=>
    [{"message"=>
       "You currently have Essential access which includes access to Twitter API v2 endpoints only. If you need access to this endpoint, you’ll need to apply for Elevated access via the Developer Portal. You can learn more here: https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api#v2-access-leve",
      "code"=>453}]},
 :uid=>""}
```
本来ならユーザーの情報が格納されていて欲しいのに、エラーメッセージが格納されています。
翻訳してみると、以下のようになりました。
> You currently have Essential access which includes access to Twitter API v2 endpoints only. If you need access to this endpoint, you’ll need to apply for Elevated access via the Developer Portal. You can learn more here: https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api#v2-access-leve
現在、Twitter API v2エンドポイントへのアクセスのみを含むEssentialアクセス権をお持ちです。このエンドポイントへのアクセスが必要な場合は、開発者ポータルからElevatedアクセスに申請する必要があります。詳しくはこちらをご覧ください: https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api#v2-access-leve

なるほど。
このメッセージに従って、アクセス権を`Essential`から`Elevated`に変更します。

最初にも載せた[この記事](https://blog.palettecms.jp/article/20103)を参考に進めていきます。
[Twitter Developer](https://developer.twitter.com/en/portal/dashboard)にアクセスすると以下のようになっていると思います。
![twitter_elavated.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1629690/d099cab6-449d-4a1c-5542-199cd64bb2bb.png)
右下を見ると、**Essential**になっています。
これを先ほどの記事を参考にして設定すると以下のようになると思います。
![スクリーンショット 2022-05-05 14.00.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1629690/abf7cc87-3624-3a57-7c1d-645821baee0b.png)
これでアクセス権を**Elevated**に変更できました。

※申し訳ございませんが、細かいやり方については省略させていただきます。

この状態で再度twitter認証を試したところ成功しました👏
ちゃんとユーザーのデータも作られていました。

# 補足
Twitter APIを使う際に、利用目的を運営側に伝え審査を受ける必要があります。
これまではネットに転がっていた例文で通っていたのですが、これが2022年4月から厳しくなったようです。
あくまで私の予想なのですが、**改正個人情報保護法**が関係していると思われます。
何度か運営側と英語でやりとりする必要がありそうですが、そこは各々でがんばってもらえればと思います。
このあたりについては詳しいことが分かれば追記しようと思います。

# 最後に

今回はこれで以上となります。
調べてもなかなか参考になる記事が出てこなかったので書かせていただきました。
少しでも参考になれば幸いです。

最後まで読んでいただきありがとうございました。