見出し画像

Rails API Twitter認証(devise token auth)

環境
・Rails 6.0.0(APIモード)
・devise token auth 1.1.3

REST API作る前にログイン機能実装したほうがのちのち楽かなと思いまして、こっちを優先しました

ここでやっていくのが、Twitter認証と管理者とユーザーのアクセス制御です
メール認証はいろいろと面倒だったのでやめました
今回認証はTwitterだけですが他にFacebookとかGoogleとかLINEとか色々ありますが基本パズガイジはTwitterやってるのでほかはいらないかもですね
まあ、ライブラリに頼るので追加も楽でしょう

Railsのログイン機能といえばdeviseが有名ですがそれのトークン認証に特化したdevise_token_authというgemがあるらしいのでこれつかっていきます
主に公式ドキュメントを参考にしました

ざっと実装手順

基本的に上の見ればいいのですがつまづいたとこもあるのでメモ程度に実装手順かいていきます
ちなみにUserモデルは作ってないところからのスタートです

gemの追加

Gemfileに以下追加してbundle install

# Gemfile
gem 'devise_token_auth'
gem 'omniauth-twitter'

トークン認証用のファイル生成

rails g devise_token_auth:install User auth

Deviseで何使うか

# app/models/user.rb
# frozen_string_literal: true

class User < ActiveRecord::Base
 # Include default devise modules. Others available are:
 # :confirmable, :lockable, :timeoutable and :omniauthable
 devise :database_authenticatable,
   :registerable,
   :recoverable,
   :rememberable,
   :trackable,
   :validatable,
   :omniauthable # ←こいつ

 include DeviseTokenAuth::Concerns::User
end

今回、OAuth認証がいるのでomniauthableを追加しました
注意点として「include DeviseTokenAuth::Concerns::User」より後にかくとダメみたいです

マイグレートファイルの修正

# db/migrate/xxxxxxxxxxx_devise_token_auth_create_users.rb
...

## User Info 追加
t.boolean :is_admin, default: false

...

## Trackable 追加
t.integer  :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string   :current_sign_in_ip
t.string   :last_sign_in_ip

...

## Confirmable 消した
# t.string   :confirmation_token
# t.datetime :confirmed_at
# t.datetime :confirmation_sent_at
# t.string   :unconfirmed_email # Only if using reconfirmable

管理者と権限分けたいのでis_admimカラムを追加しました。
それと、ユーザー分析に便利なTrackableが↑をみるにデフォルトで有効になってますが、必要なカラムが書き込まれていないので、↑の:trackableをけすか、カラムを追加する必要がありました。これバグなのでしょうか?
あと、メール認証しないのでConfirmableに必要なカラムはコメントアウトしてます(こっちはデフォルトで無効なのに・・・)

マイグレーション

rake db:migrate

ここでエラーが出ました

NoMethodError: undefined method `devise' for User (call 'User.connection' to establish a connection):Class

Userモデルにextend Devise::Modelsを追記すればいいらしい。なんでかは不明。再度マイグレーションしたら成功しました

# app/models/user.rb
# frozen_string_literal: true

class User < ActiveRecord::Base
 extend Devise::Models # ←こいつ
 # Include default devise modules. Others available are:
 # :confirmable, :lockable, :timeoutable and :omniauthable
 devise :database_authenticatable,
   :registerable,
   :recoverable,
   :rememberable,
   :trackable,
   :validatable,
   :omniauthable

 include DeviseTokenAuth::Concerns::User
end

devise token authの設定

# config/initializers/devise_token_auth.rb
# frozen_string_literal: true

DeviseTokenAuth.setup do |config|
 config.change_headers_on_each_request = false
end

change_headers_on_each_requestをfalseにしたのはリクエストのたびにトークンを変えなくするためです
テストでトークンを流用したいので一時的にfalseに。テスト終わったらtrueにします

Twitter認証の設定

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, 'key', 'secret'
end

コメント 2020-03-03 220515

'key'と'secret'の場所にTwitterのダッシュボードにかいてあるやついれます。このへんのキーはハードコーディングしないほうがいいので環境変数から読み込んどいたほうがいいですね
それとTwitter認証後のコールバックURLをhttp://~~~/omniauth/twitter/callbackに設定してあげます。

CORS設定

# config/application.rb   
config.middleware.use Rack::Cors do
  allow do
    origins '*'
    resource '*',
    headers: :any,
      expose: ['access-token', 'expiry', 'token-type', 'uid', 'client'],
      methods: [:get, :post, :options, :delete, :put]
  end
end

別ドメイン間許可の設定です。開発時は全許可でいいとおもうのでドキュメントからコピペしました。ここで重要なのはexposeのとこでしょう。

セッション・クッキー設定

# config/application.rb
config.session_store :cookie_store, key: '_session_mechaco'
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options

railsをAPIモードで作るとセッション機能が無効化されているので、有効にする必要がありました。とりあえずファイルストアでセッション管理する感じです

Twitter認証してみる

さっそくTwitter認証でユーザー作ってみます。

/auth/twitter にアクセスするとアプリ認証画面が出てきて許可するとアカウント作成される予定でしたが「ActiveModel::ForbiddenAttributesError」というエラーがでました
原因はStrongParametersにひっかかってるみたいです

対処方法としてまずomniauthのコールバックを処理するコントローラをオーバーライドするので、config/route.rbをかきかえます

# config/route.rb

# Before
# mount_devise_token_auth_for 'User', at: 'auth'

# After
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
  omniauth_callbacks: 'overrides/omniauth_callbacks'
}

オーバーライド先のファイルを作ります

# app/controllers/concerns/overrides/omniauth_callbacks_controller.rb
module Overrides

 class OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController

   def redirect_callbacks
     super
   end

   def omniauth_success
     super
   end

   def omniauth_failure
     super
   end

   protected
   def assign_provider_attrs(user, auth_hash)
     case auth_hash['provider']
     when 'twitter'
       user.assign_attributes({
         nickname: auth_hash['info']['nickname'],
         name: auth_hash['info']['name'],
         image: auth_hash['info']['image'],
         email: auth_hash['info']['email']
       })
     else
       super
     end
   end
 end

end

上記だとTwitterしか対応してませんのでFacebookとかも必要な場合case文の分岐増やして適宜attributeセットしてあげてください
あと、オーバーライドが弊害して、routingされるpublicメソッドはsuperで再定義してあげないといけないみたいです。これのせいでレスポンスヘッダーにトークンが格納されず、2時間くらいつまってました・・・

これで再度/auth/twitterへアクセス

コメント 2020-03-04 225104

Authorize appしたら無事アカウントも作成されTwitterで設定したhttp://~~~/omniauth/twitter/callbackへリダイレクトしてくれました
ログやDBみるとnameにTwitterID、nicknameにTwitter表示名、imageにアイコンURLが格納されています。

で、レスポンスヘッダーにaccess-token、uid、clientが格納されていて、これを次のリクエストから使ってあげると認証できます
ヘッダーの確認はChrome developerツールのNetworkタブでみれます

ログイン制御を試す

「authenticate_user!」でログイン制御が行えます
ログインしてないとアクセスできないパスを簡単に作りました

# app/controller/auth_tests_controller.rb
class AuthTestsController < ApplicationController
  before_action :authenticate_user!, only: :index

  def index
    render json: current_user
  end

end
# config/routes.rb
# 追記
get '/auth_test', to: 'auth_tests#index'

で、/auth_test にGETでリクエスト送ります。ヘッダーには↑↑↑でとれたaccess-token、uid、clientをセットしてあげます。
こんな感じのレスポンスがきてればちゃんと認証されています。

{
   "id": 1,
   "provider": "twitter",
   "uid": "00000000000000000000",
   "allow_password_change": false,
   "name": "osufu",
   "nickname": "osufu",
   "image": "http://~~~~.png",
   "email": null,
   "is_admin": false,
   "created_at": "0000-00-00T00:00:00.000+09:00",
   "updated_at": "0000-00-00T00:00:00.000+09:00"
}

今度はトークンを間違えたりヘッダーにセットしないでリクエスト送ってみます。

{
   "errors": [
       "You need to sign in or sign up before continuing."
   ]
}

エラーが出てくれました。

管理者の制御

次はログインできていてなおかつ管理者権限のある人だけがアクセスできるようにします。

# app/controller/application_controller.rb
class ApplicationController < ActionController::API
  include DeviseTokenAuth::Concerns::SetUserByToken

  protected
  def admin_user!
    unless user_signed_in? && current_user.is_admin
      render json: { message: 'not found' }, status: 404
      return
    end
  end

end
# app/controller/auth_tests_controller.rb
class AuthTestsController < ApplicationController
  before_action :authenticate_user!, only: :index
  before_action :admin_user!, only: :admin_only

  def index
    render json: current_user
  end

  def admin_only
   render json: { message: 'You are admin!' }
  end

end
# config/routes.rb
# 追記
get '/auth_test_admin', to: 'auth_tests#admin_only'

「admin_user!」で管理者制御するようにしました。

まださっき作ったユーザーに管理者権限を持たせていないので、正常なトークンをいれて/auth_test_adminにアクセスしても以下のようなエラーが返されるようになります

{
   "message": "not found"
}

一応、管理者が使うAPIと推測されたくないため401じゃなくて404エラーにしています。

管理者権限を持たせてみます。DB直接いじるかrails cでUser.find(1).update(is_admin: true)をうつでもよいです。
で、アクセスすると

{
   "message": "You are admin!"
}

管理者としてみなされました。

結構バグ?らしきものがあって、かつ参考記事も少なかったので調査に時間がかかりました。自前でトークン認証実装するより断然楽なので良かったですが、予想より面倒でした。

それでは👋😉


この記事が気に入ったらサポートをしてみませんか?