This page looks best with JavaScript enabled

【Rails】SorceryでTwitter認証

 ·   ·  ☕ 8 分で読めます

はじめに

Sorceryを使ったTwitterログイン認証の設定です。
基本的にはwikiの通りにすればいいのですが、変なハマり方をしたので、次回の自分への自戒を込めて記録しておきます。
また、コールバックURLなどについて公式ドキュメントを漁ることになったので、日本語でまとめておくことにします。

動作環境

ruby 2.5.3
Rails 5.2.4.2
sorcery 0.14.0

前提

  • Sorceryによるログイン機能
  • Twitter Develpersのユーザー登録とアプリの登録

簡単な流れ

Sorceryのwikiにチュートリアルがあるので、基本的にはそれに従っていきます。

authenticationsテーブルを作成

1
2
3
4
$ rails g sorcery:install external --only-submodules
        gsub  config/initializers/sorcery.rb
      insert  app/models/user.rb
      create  db/migrate/20200203110653_sorcery_external.rb

コマンドを打つと、外部認証に必要なauthenticationsテーブルを作成するためのマイグレーションファイルが作られます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class SorceryExternal < ActiveRecord::Migration
  def change
    create_table :authentications do |t|
      t.integer :user_id, :null => false
      t.string :provider, :uid, :null => false

      t.timestamps
    end

    add_index :authentications, [:provider, :uid]
    add_index :authentications, :user_id # user_idにindexを貼る場合は追加
  end
end
1
$ bundle exec rails db:migrate

Authenticationモデルの設定

1
$ rails g model Authentication --migration=false

先ほど既にマイグレーションファイルは作成しているので、--migration=falseオプションを付けています。

1
2
3
class Authentication < ActiveRecord::Base
  belongs_to :user
end
1
2
3
4
5
class User < ApplicationRecord
  authenticates_with_sorcery!
  has_many :authentications, dependent: :destroy
  accepts_nested_attributes_for :authentications # has_many :authenticationsより下に書く
end

ここまでは外部認証共通の処理となります。

sorcery.rbの設定

1
2
3
4
5
6
7
8
  config.twitter.key = Rails.application.credentials.dig(:twitter, :key)
  config.twitter.secret = Rails.application.credentials.dig(:twitter, :secret_key)
  config.twitter.callback_url = 'http://localhost:3000/oauth/callback?provider=twitter'
  config.twitter.user_info_mapping = {
    twitter_id: 'id',
    name: 'name',
    description: 'description'
  } # Userモデルの属性名: 'twitterのパラメータ'

config.twitter.callback_urlは環境によって変わるはずなので、Configなどを利用してください。

1
2
3
twitter:
  key: API keyの値
  secret_key: API secret keyの値
スクリーンショット 2020-04-11 7.55.44.png Twitterのscreen_name(`@username`というやつ)は変わる可能性があるので、twitter_idカラムにユーザーidを格納することにしました。

Twitter Developer の設定

「Enable Sign in with Twitter」にチェックを入れます。
Callback URLsは以下のベストプラクティス(クエリを含まないこと・ローカルホストにしないこと)に則り、http://127.0.0.1:3000/oauth/callbackにします。

Do not add query strings to your callback URLs in your Twitter app’s configuration
Don’t use localhost as a callback URL
Callback URLs — Twitter Developers<br />

image.png

上のCallback URLではクエリ文字列が使えないので、Sorceryの設定のcallback_urlでクエリを渡します。
すると、認証画面のURLにoauth_callbackというクエリが渡され、クエリを持ったコールバックURLを設定されていることがわかります。
image.png

認証画面の URL

※注意
ローカルでアクセスする際はlocalhost:3000ではなく127.0.0.1:3000にアクセスするようにしてください。
参考:Twitterログイン 401 Authorization Required

メールアドレスを取得する場合

メールアドレスを取得する場合、プライバシーポリシーと利用規約のページを登録してパーミッションをする必要があります。
URLはApp detailsのページで登録してください。ローカルのURLは登録できないので、開発中は暫定的にGitHubのリポジトリのURLを入れるといいです。
そして、以下のAdditional Permissionsのチェックを入れたらTwitter Developerの作業は終わりです。
スクリーンショット 2020-04-23 13.13.54.png

sorcery.rbには以下の二行を追加します。
user_info_pathはデフォルトでは'/1.1/account/verify_credentials.json'になるので、クエリの入った値を代入します。また、'email'にデータが格納されるので、user.emailに保存します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  config.twitter.key = Rails.application.credentials.dig(:twitter, :key)
  config.twitter.secret = Rails.application.credentials.dig(:twitter, :secret_key)
  config.twitter.callback_url = 'http://127.0.0.1:3000/oauth/callback?provider=twitter'
  config.twitter.user_info_path = "/1.1/account/verify_credentials.json?include_email=true" # 追加する
  config.twitter.user_info_mapping = {
    email: 'email', # 追加する
    twitter_id: 'id',
    name: 'name',
    description: 'description'
  }

※注意
Twitterではすべてのユーザーがメールアドレスを登録しているわけではありません。そのため、user.emailにはnilが入ることも想定して実装してください。

また、これを行った後、もしかしたらAccess tokenとAccess token secret周辺でエラーが起きるかもしれません。
(原因がこれかはわからないが、APIのエラーが出た。再度作り直せば問題ない)

参考:Sorceryを使ったTwitterログイン
   GET account / verify_credentials — Twitter Developers

Oauth処理を行うコントローラを作成

1
$ rails g controller Oauths oauth callback
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class OauthsController < ApplicationController
  skip_before_action :require_login # applications_controllerでbefore_action :require_loginを設定している場合

  def oauth
    login_at(auth_params[:provider])
  end

  def callback
    provider = auth_params[:provider]
    if (@user = login_from(provider))
      redirect_to root_path, notice: "#{provider.titleize}でログインしました"
    else
      begin
        @user = create_from(provider)
        reset_session
        auto_login(@user)
        redirect_to root_path, notice: "#{provider.titleize}でログインしました"
      rescue StandardError
        redirect_to root_path, alert: "#{provider.titleize}でのログインに失敗しました"
      end
    end
  end

  private

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

基本的にはチュートリアルの通りです。多少、Rails5の書き方に直します。

ルーティングの追加

1
2
3
post "oauth/callback", to: "oauths#callback"
get "oauth/callback", to: "oauths#callback"
get "oauth/:provider", to: "oauths#oauth", as: :auth_at_provider
1
2
3
oauth_callback POST   /oauth/callback(.:format)              oauths#callback
                GET    /oauth/callback(.:format)             oauths#callback
auth_at_provider GET    /oauth/:provider(.:format)           oauths#oauth

ログインボタンを追加

1
<%= link_to 'Login with Twitter', auth_at_provider_path(provider: :twitter) %>

キャンセル後のリダイレクト処理

今の状態では、「連携アプリをキャンセル」を押したときにエラーが出てしまうので、それを解消します。
スクリーンショット 2020-03-28 23.41.01.png
ここで「(アプリ名)に戻る」を押すと、下のように401エラーになってしまいます。

1
2
3
4
5
6
7
8
Started GET "/oauth/callback?provider=twitter&denied=gZtRIQAAAAABDRHmAAABcSN_g4w" for 127.0.0.1 at 2020-03-29 08:34:55 +0900
Processing by OauthsController#callback as HTML
  Parameters: {"provider"=>"twitter", "denied"=>"gZtRIQAAAAABDRHmAAABcSN_g4w"}
Unpermitted parameter: :denied
Completed 500 Internal Server Error in 400ms (ActiveRecord: 0.0ms)

OAuth::Unauthorized - 401 Authorization Required:
  app/controllers/oauths_controller.rb:12:in `callback'
1
<ActionController::Parameters {"provider"=>"twitter", "denied"=>"PsylCwAAAAABDRHmAAABcSGWp1Q", "controller"=>"oauths", "action"=>"callback"} permitted: false>

キャンセルした場合はparams[:denied]が存在するので、条件分岐で処理します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class OauthsController < ApplicationController
  def oauth
    login_at(auth_params[:provider])
  end

  def callback
    provider = auth_params[:provider]
    if auth_params[:denied].present?  # ここの節を追加
      redirect_to root_path, notice: 'ログインをキャンセルしました'
      return
    end
    if (@user = login_from(provider))
      redirect_to root_path, notice: "#{provider.titleize}でログインしました"
    else
      begin
        @user = create_from(provider)
        reset_session
        auto_login(@user)
        redirect_to root_path, notice: "#{provider.titleize}でログインしました"
      rescue StandardError
        redirect_to root_path, alert: "#{provider.titleize}でのログインに失敗しました"
      end
    end
  end

  private

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

callbackメソッドが長くなり、ネストも深くなってしまったので、create_user_fromメソッドをprivate以下に切り分けます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class OauthsController < ApplicationController
  # (省略)

  def callback
    provider = auth_params[:provider]
    if auth_params[:denied].present?
      redirect_to root_path, notice: "#{provider.titleize}でログインしました"
      return
    end
    create_user_from(provider) unless (@user = login_from(provider))
    redirect_to root_path, notice: "#{provider.titleize}でログインしました"
  end

  private

  # (省略)

  def create_user_from(provider)
    @user = create_from(provider)
    reset_session
    auto_login(@user)
  rescue StandardError
    redirect_to root_path, alert: "#{provider.titleize}でのログインに失敗しました"
  end
end

以上で設定は終了です!
以下は、ちょっと試してみたことやエラーとの奮闘になります。

callback_urlを設定しない場合

For OAuth 1.0a compliance this parameter(注釈:oauth_callbackのこと) is required .
POST oauth/request_token — Twitter Developers

ということなので、callback_urlを設定しないとどうなるのか試してみた。

1
  # config.twitter.callback_url = 'http://localhost:3000/oauth/callback?provider=twitter'

認証画面のURL

クエリを見ると、oauth_callbackに値が入っていない。
認証を押すと、PINコードの入力を求められ、リダイレクトしなくなってしまった。
image.png

エラーいろいろ

NoMethodError at /oauth/callback

認証後に/oauth/callbackに行かない。
image.png

NoMethodError at /oauth/callback
undefined method `original_callback_url' for nil:NilClass

コールバック時のURL

http://127.0.0.1:3000/oauth/callback?provider=twitter&oauth_token=EHBQ4wAAAAABDRHmAAABcRr4LwQ&oauth_verifier=zFvTO1ay9G316hmbPMcbexTfckA45M4j

画面ばかり見て「どうしてoauthsアクションに飛んでいるのだろう」と思っていたのですが、そもそもNoMethodErrorでした。

解決法

1
2
3
  post "oauth/callback", to: "oauths#callback"
  get 'oauth/callback', to: 'oauths#callback' # この行がなかったので追加
  get "oauth/:provider", to: "oauths#oauth", as: :auth_at_provider

Mysql2::Error - Field ‘twitter_id’ doesn’t have a default value:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Started GET "/oauth/callback?provider=twitter&oauth_token=qvWE_gAAAAABDRHmAAABcSFi3bQ&oauth_verifier=nUb5kYYSryTHA03FepYw2xSLNzVFMmTc" for 127.0.0.1 at 2020-03-28 22:44:22 +0900
Processing by OauthsController#callback as HTML
  Parameters: {"provider"=>"twitter", "oauth_token"=>"qvWE_gAAAAABDRHmAAABcSFi3bQ", "oauth_verifier"=>"nUb5kYYSryTHA03FepYw2xSLNzVFMmTc"}
Unpermitted parameters: :oauth_token, :oauth_verifier
  Authentication Load (0.8ms)  SELECT  `authentications`.* FROM `authentications` WHERE `authentications`.`uid` = '1048451188209770497' AND `authentications`.`provider` = 'twitter' ORDER BY `authentications`.`id` ASC LIMIT 1
  ↳ app/controllers/oauths_controller.rb:8
   (0.2ms)  BEGIN
  ↳ app/controllers/oauths_controller.rb:13
  User Exists (3.4ms)  SELECT  1 AS one FROM `users` WHERE `users`.`uuid` = '7gl0ZsQ_tTbl' LIMIT 1
  ↳ app/models/user.rb:21
  User Create (1.5ms)  INSERT INTO `users` (`uuid`, `name`, `description`, `created_at`, `updated_at`) VALUES ('7gl0ZsQ_tTbl', 'aiandrox', '小学校の先生やってた。エンジニア目指してRailsとか頑張ってる。#RUNTEQ 1月生。謎解き好き。', '2020-03-28 22:44:24', '2020-03-28 22:44:24')
  ↳ app/controllers/oauths_controller.rb:13
   (0.2ms)  ROLLBACK
  ↳ app/controllers/oauths_controller.rb:13
Completed 500 Internal Server Error in 1349ms (ActiveRecord: 42.5ms)

Mysql2::Error - Field 'twitter_id' doesn't have a default value:
  app/controllers/oauths_controller.rb:13:in `callback'

データベース側で twitter_id カラムにnull: false制約をかけていたので、このエラーが出ました。
確かにログを見ると twitter_id カラムには何も入っていない。

解決法

1
2
3
4
5
6
  config.twitter.user_info_mapping = {
-   twitter_id: 'user_id',
+   twitter_id: 'id',
    name: 'name',
    description: 'description'
  }

Twitterから取得するデータのパラメータが間違っていました。
参考:User Object(ユーザーオブジェクト)の説明

Share on

aiandrox
Written by
aiandrox
今日も楽しく明日も楽しく

目次