データ移行せずに、登録済みのユーザがそのままAPIを使ったアプリでも認証できるようにDevise Token Authを入れて共存できるようにしてみました。
今まで通りDeviseで登録・認証できる事、APIでも登録・認証できるようにするのが、今回のゴールです。
Devise Token Auth導入
TODO: rack-corsの設定(APIのみ許可するようにした方が良さそう)
Gemfile
gem 'devise_token_auth' gem 'rack-cors'
% bundle install % rails g devise_token_auth:install User auth Running via Spring preloader in process 63548 create config/initializers/devise_token_auth.rb insert app/controllers/application_controller.rb gsub config/routes.rb create db/migrate/20210806000715_devise_token_auth_create_users.rb insert app/models/user.rb
migrateファイル書き替え
新規作成のスキーマになっているので、追加するカラムとインデックスのみに変更する。
db/migrate/20210806000715_devise_token_auth_create_users.rb を削除
db/migrate/20210806000715_devise_token_auth_change_users.rb を作成
class DeviseTokenAuthChangeUsers < ActiveRecord::Migration[6.1] def change ## Required add_column :users, :provider, :string, null: false, default: 'email', comment: '認証方法' add_column :users, :uid, :string, null: false, default: '', comment: 'UID' ActiveRecord::Base.connection.execute('UPDATE users SET uid = email') ## Recoverable add_column :users, :allow_password_change, :boolean, default: false, comment: 'パスワード変更許可' ## User Info add_column :users, :nickname, :string, comment: 'ニックネーム' ## Tokens add_column :users, :tokens, :text, comment: 'トークン' add_index :users, [:uid, :provider], unique: true, name: 'index_users7' end end
「UPDATE users SET uid = email」は、下記でも同じですが、1回の方が早いので、SQL直書きにしています。
uidは、providerが'email'の場合、メールアドレスが入る。
User.reset_column_information User.find_each do |user| user.update_column(:uid, user.email) end
nameは既に追加済みなので記載していません。無い場合は追加してください。
add_column :users, :name, :string, comment: '氏名'
db:migrate
% rails db:migrate Could not load 'omniauth'. Please ensure you have the omniauth gem >= 1.0.0 installed and listed in your Gemfile. rails aborted! LoadError: cannot load such file -- omniauth
元々、omniauth使ってないので、Gemが無いと言われました。
app/models/user.rb
class User < ApplicationRecord - # Include default devise modules. - devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :trackable, :validatable, - :confirmable, :omniauthable - include DeviseTokenAuth::Concerns::User devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable, :lockable, :timeoutable, :trackable + include DeviseTokenAuth::Concerns::User
% rails db:migrate rails aborted! ArgumentError: Invalid route name, already in use: 'new_user_registration'
既に存在するモデル(今回はUser)にしたので、routeの名前がバッティングしました。
一旦、コメントアウトして、後ほど直します。
config/routes.rb
Rails.application.routes.draw do - mount_devise_token_auth_for 'User', at: 'auth' + # mount_devise_token_auth_for 'User', at: 'auth'
% rails db:migrate == 20210806000715 DeviseTokenAuthChangeUsers: migrating ======================= -- add_column(:users, :provider, :string, {:null=>false, :default=>"email", :comment=>"認証方法"}) -> 0.2292s -- add_column(:users, :uid, :string, {:null=>false, :default=>"", :comment=>"UID"}) -> 0.1201s -- add_column(:users, :allow_password_change, :boolean, {:default=>false, :comment=>"パスワード変更許可"}) -> 0.1629s -- add_column(:users, :nickname, :string, {:comment=>"ニックネーム"}) -> 0.1939s -- add_column(:users, :tokens, :text, {:comment=>"トークン"}) -> 0.2547s -- add_index(:users, [:uid, :provider], {:unique=>true, :name=>"index_users7"}) -> 0.1820s == 20210806000715 DeviseTokenAuthChangeUsers: migrated (1.1431s) ==============
成功!
確認
MySQL
% rails db > SELECT id, provider, uid, allow_password_change, nickname, tokens FROM users; +----+----------+----------------+-----------------------+----------+--------+ | id | provider | uid | allow_password_change | nickname | tokens | +----+----------+----------------+-----------------------+----------+--------+ | 1 | email | user1@mydomain | 0 | NULL | NULL | | 2 | email | user2@mydomain | 0 | NULL | NULL | | 3 | email | user3@mydomain | 0 | NULL | NULL | +----+----------+----------------+-----------------------+----------+--------+ 3 rows in set (0.005 sec)
UPDATE文で、uidにemailの値が入りました。
defaultのemail(provider)や0(allow_password_change)も入っているので、
add_columnでのdefault指定は、既存データも更新してくれる。
PostgreSQL
こちらも同様でOK!
% rails db # SELECT id, provider, uid, allow_password_change, nickname, tokens FROM users; id | provider | uid | allow_password_change | nickname | tokens ----+----------+----------------+-----------------------+----------+-------- 1 | email | user1@mydomain | f | | 2 | email | user2@mydomain | f | | 3 | email | user3@mydomain | f | | (3 rows)
SQLite3
こちらも同様でOK!
% rails db > SELECT id, provider, uid, allow_password_change, nickname, tokens FROM users; 1|email|user1@mydomain|0|| 2|email|user2@mydomain|0|| 3|email|user3@mydomain|0||
db:seed修正
db/seed/development/users.yml
(複数ある場合は、同様に修正)
- email: 'user1@mydomain' + email: 'user1@mydomain.com' + uid: 'user1@mydomain.com'
メアドのドメインをmydomain.comに変えたのは、失敗(rails aborted!)する為。
確認
% rails c > User.find(1).destroy TRANSACTION (20.0ms) COMMIT % rails db:seed "[1/3] id: 1 ... Create"
routeを通す
そのままだとバッディングするのと、
パスを/users/authにしたいので、namespaceの中に入れます。
config/routes.rb
- # mount_devise_token_auth_for 'User', at: 'auth' + namespace :users do + mount_devise_token_auth_for 'User', at: 'auth' + end
確認
% rails routes new_users_user_session GET /users/auth/sign_in(.:format) devise_token_auth/sessions#new users_user_session POST /users/auth/sign_in(.:format) devise_token_auth/sessions#create destroy_users_user_session DELETE /users/auth/sign_out(.:format) devise_token_auth/sessions#destroy new_users_user_password GET /users/auth/password/new(.:format) devise_token_auth/passwords#new edit_users_user_password GET /users/auth/password/edit(.:format) devise_token_auth/passwords#edit users_user_password PATCH /users/auth/password(.:format) devise_token_auth/passwords#update PUT /users/auth/password(.:format) devise_token_auth/passwords#update POST /users/auth/password(.:format) devise_token_auth/passwords#create cancel_users_user_registration GET /users/auth/cancel(.:format) devise_token_auth/registrations#cancel new_users_user_registration GET /users/auth/sign_up(.:format) devise_token_auth/registrations#new edit_users_user_registration GET /users/auth/edit(.:format) devise_token_auth/registrations#edit users_user_registration PATCH /users/auth(.:format) devise_token_auth/registrations#update PUT /users/auth(.:format) devise_token_auth/registrations#update DELETE /users/auth(.:format) devise_token_auth/registrations#destroy POST /users/auth(.:format) devise_token_auth/registrations#create new_users_user_confirmation GET /users/auth/confirmation/new(.:format) devise_token_auth/confirmations#new users_user_confirmation GET /users/auth/confirmation(.:format) devise_token_auth/confirmations#show POST /users/auth/confirmation(.:format) devise_token_auth/confirmations#create new_users_user_unlock GET /users/auth/unlock/new(.:format) devise_token_auth/unlocks#new users_user_unlock GET /users/auth/unlock(.:format) devise_token_auth/unlocks#show POST /users/auth/unlock(.:format) devise_token_auth/unlocks#create users_auth_validate_token GET /users/auth/validate_token(.:format) devise_token_auth/token_validations#validate_token
CSRFトークン
% rails s
% curl http://localhost:3000/users/auth -H "Content-Type: application/json" -X POST -d '{ "email":"test@mydomain.com", "password":"abc12345"}' <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Action Controller: Exception caught</title> 〜省略〜 <header> <h1> ActionController::InvalidAuthenticityToken in DeviseTokenAuth::RegistrationsController#create </h1> </header>
InvalidAuthenticityToken → CSRFトークンチェックでエラー
APIではページがないので、チェックできない。(認証トークンとかで担保かな)
CSRFトークンの仕組みはこちら
ActionController::InvalidAuthenticityTokenの解消(解決済み)
部分的にCSRFチェックを外す
ApplicationControllerでやったり、devise_controller?で判断すると、
既存のページやDeviseも外れちゃうので、それぞれのControllerを作る事にしました。
序でにこれも移動
app/controllers/application_controller.rb
- include DeviseTokenAuth::Concerns::SetUserByToken
config/routes.rb
namespace :users do - mount_devise_token_auth_for 'User', at: 'auth' + mount_devise_token_auth_for 'User', at: 'auth', controllers: { + registrations: 'users/auth/registrations', + confirmations: 'users/auth/confirmations', + sessions: 'users/auth/sessions', + unlocks: 'users/auth/unlocks', + passwords: 'users/auth/passwords', + token_validations: 'users/auth/token_validations' + } end
app/controllers/users/auth/registrations_controller.rb を作成
class Users::Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token end
app/controllers/users/auth/confirmations_controller.rb を作成
class Users::Auth::ConfirmationsController < DeviseTokenAuth::ConfirmationsController include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token end
app/controllers/users/auth/sessions_controller.rb を作成
class Users::Auth::SessionsController < DeviseTokenAuth::SessionsController include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token end
app/controllers/users/auth/unlocks_controller.rb を作成
class Users::Auth::UnlocksController < DeviseTokenAuth::UnlocksController include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token end
app/controllers/users/auth/passwords_controller.rb を作成
class Users::Auth::PasswordsController < DeviseTokenAuth::PasswordsController include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token end
app/controllers/users/auth/token_validations_controller.rb を作成
class Users::Auth::TokenValidationsController < DeviseTokenAuth::TokenValidationsController include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token end
確認
% rails routes new_users_user_session GET /users/auth/sign_in(.:format) users/auth/sessions#new users_user_session POST /users/auth/sign_in(.:format) users/auth/sessions#create destroy_users_user_session DELETE /users/auth/sign_out(.:format) users/auth/sessions#destroy new_users_user_password GET /users/auth/password/new(.:format) users/auth/passwords#new edit_users_user_password GET /users/auth/password/edit(.:format) users/auth/passwords#edit users_user_password PATCH /users/auth/password(.:format) users/auth/passwords#update PUT /users/auth/password(.:format) users/auth/passwords#update POST /users/auth/password(.:format) users/auth/passwords#create cancel_users_user_registration GET /users/auth/cancel(.:format) users/auth/registrations#cancel new_users_user_registration GET /users/auth/sign_up(.:format) users/auth/registrations#new edit_users_user_registration GET /users/auth/edit(.:format) users/auth/registrations#edit users_user_registration PATCH /users/auth(.:format) users/auth/registrations#update PUT /users/auth(.:format) users/auth/registrations#update DELETE /users/auth(.:format) users/auth/registrations#destroy POST /users/auth(.:format) users/auth/registrations#create new_users_user_confirmation GET /users/auth/confirmation/new(.:format) users/auth/confirmations#new users_user_confirmation GET /users/auth/confirmation(.:format) users/auth/confirmations#show POST /users/auth/confirmation(.:format) users/auth/confirmations#create new_users_user_unlock GET /users/auth/unlock/new(.:format) users/auth/unlocks#new users_user_unlock GET /users/auth/unlock(.:format) users/auth/unlocks#show POST /users/auth/unlock(.:format) users/auth/unlocks#create users_auth_validate_token GET /users/auth/validate_token(.:format) users/auth/token_validations#validate_token
devise_token_auth/auth/になっていたのが、users/auth/に変わりました。
作成したControllerが呼ばれています。
パラメータ追加
% curl http://localhost:3000/users/auth -H "Content-Type: application/json" -X POST -d '{ "email":"test@mydomain.com", "password":"abc12345"}' {"success":false, "errors":["'confirm_success_url' パラメータが与えられていません。"],"status":"error","data":{"id":null,"code":"aaa0eb67dd9db897927ee58b8d6bf2e3","image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"name":null,"email":"test@mydomain.com","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":null,"updated_at":null,"provider":"email","uid":"test@mydomain.com","allow_password_change":false,"nickname":null}}
confirm_success_urlを指定してみる。用途は後ほど。
% curl http://localhost:3000/users/auth -H "Content-Type: application/json" -X POST -d '{ "email":"test@mydomain.com", "password":"abc12345", "confirm_success_url":"http://localhost:3000/users/auth/sign_in"}' {"status":"error","data":{"id":null,"code":null,"image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"name":null,"email":"test@mydomain.com","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":null,"updated_at":null,"provider":"email","uid":"","allow_password_change":false,"nickname":null}, "errors":{"code":["お問い合わせ頂くか、最初からやり直してください。"],"name":["入力してください。"],"full_messages":["ユーザーコード お問い合わせ頂くか、最初からやり直してください。","氏名 入力してください。"]}}
独自にcodeを割り当てたり、nameにvalidates掛けているので、修正とパラメータ追加。
app/controllers/users/auth/registrations_controller.rb に追加
before_action :configure_sign_up_params, only: %i[create] def create params[:code] = create_unique_code(User, 'code', "Users::RegistrationsController.create #{params}") ActiveRecord::Base.transaction do # Tips: エラーでROLLBACKされない為 super end end protected # If you have extra params to permit, append them to the sanitizer. def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up, keys: %i[code name]) end
パラメータ追加は、下記メソッドを追加しちゃっても出来るけど、
Deviseと同様に、devise_parameter_sanitizerに設定すれば効きます。
gems/devise_token_auth-1.2.0/app/controllers/devise_token_auth/registrations_controller.rb
def sign_up_params params.permit(*params_for_resource(:sign_up)) end
% curl http://localhost:3000/users/auth -H "Content-Type: application/json" -X POST -d '{ "name":"test", "email":"test@mydomain.com", "password":"abc12345", "confirm_success_url":"http://localhost:3000/users/auth/sign_in"}' {"status":"success","data":{"id":30,"code":"31e210e8ede22e1a7053107db1dcd5ea","image":{"url":null,"mini":{"url":null},"small":{"url":null},"medium":{"url":null},"large":{"url":null},"xlarge":{"url":null}},"name":"test","email":"test@mydomain.com","destroy_requested_at":null,"destroy_schedule_at":null,"created_at":"2021-08-08T14:52:16.934+09:00","updated_at":"2021-08-08T14:52:16.934+09:00","provider":"email","uid":"test@mydomain.com","allow_password_change":false,"nickname":null}}
成功!
確認
% rails db > SELECT * FROM users WHERE email = 'test@mydomain.com'\G *************************** 1. row *************************** code: cdb<省略> name: test email: test@mydomain.com encrypted_password: $2a<省略> confirmation_token: sKx<省略> confirmed_at: NULL confirmation_sent_at: 2021-08-08 06:01:04 provider: email uid: test@mydomain.com allow_password_change: 0 nickname: NULL tokens: NULL
rails sのコンソールやLetterOpenerWebで確認
Subject:【開発環境】【RailsAppOrigin】メールアドレス確認のお願い To: test@mydomain.com
メールのURLアクセスで、認証OK
既存画面でのログイン・ログアウトもOK
RSpecで既存テスト
% rspec Failures: 1) Users::Registrations PUT #update ログイン中 behaves like [ログイン中]有効なパラメータ behaves like ToTop トップページにリダイレクト Failure/Error: expect(flash[:notice]).to notice.present? ? eq(I18n.t(notice)) : be_nil expected: "メールアドレス確認のメールを送信しました。メールを確認してください。" got: "登録情報を変更しました。" (compared using ==) Shared Example Group: "ToTop" called from ./spec/requests/users/registrations_spec.rb:227 Shared Example Group: "[ログイン中]有効なパラメータ" called from ./spec/requests/users/registrations_spec.rb:259 # ./spec/requests/users/registrations_spec.rb:206:in `block (4 levels) in' Finished in 1 minute 9.63 seconds (files took 4.05 seconds to load) 807 examples, 1 failure Failed examples: rspec './spec/requests/users/registrations_spec.rb[1:4:2:1:2:1]' # Users::Registrations PUT #update ログイン中 behaves like [ログイン中]有効なパラメータ behaves like ToTop トップページにリダイレクト
config/initializers/devise_token_auth.rb
+ config.send_confirmation_email = true
% rspec spec/requests/users/registrations_spec.rb Finished in 5.28 seconds (files took 1.3 seconds to load) 63 examples, 0 failures
OK。テストがあると便利ですね!
但し、現状、メールが送信されたかまではテストしていない。
メッセージは変わったけど、実際には送信されていない。
Modelで対応:NG
APIだと、同じメールが2通送信されちゃいました。
上記のconfig.send_confirmation_emailをfalseにしても変わらず。
app/models/user.rb
+ after_create :send_confirmation_instructions
Controllerで対応:OK
ベストでは無いけど、既存のRegistrationsControllerで対応。
1通送信されました。
APIはここに来ないので影響なし。1通送信。
app/controllers/users/registrations_controller.rb
def create params[:user][:code] = create_unique_code(User, 'code', "Users::RegistrationsController.create #{params[:user]}") super flash[:alert] = resource.errors[:code].first if resource.errors[:code].present? + resource.send_confirmation_instructions if resource.errors.blank? # Tips: devise_token_auth導入後、送信されなくなった為 end
設定ファイル確認
上記で変更した以外は、デフォルトでも問題なさそうなので、後は必要になってから変える事にします。
下記だけ気になりましたが、Controllerで明確に分けているので、falseのままで良さそう。
experimental(実験的)ってのも避けたくなる記載。
# By default, only Bearer Token authentication is implemented out of the box. # If, however, you wish to integrate with legacy Devise authentication, you can # do so by enabling this flag. NOTE: This feature is highly experimental! # config.enable_standard_devise_support = false
デフォルトでは、BearerToken認証のみがすぐに実装されます。 ただし、従来のDevise認証と統合する場合は、このフラグを有効にすることで統合できます。 注:この機能は非常に実験的なものです。
gems/devise_token_auth-1.2.0/app/controllers/devise_token_auth/concerns/set_user_by_token.rb
# check for an existing user, authenticated via warden/devise, if enabled if DeviseTokenAuth.enable_standard_devise_support devise_warden_user = warden.user(mapping) if devise_warden_user && devise_warden_user.tokens[@token.client].nil? @used_auth_by_token = false @resource = devise_warden_user # REVIEW: The following line _should_ be safe to remove; # the generated token does not get used anywhere. # @resource.create_new_auth_token end end
最後に
長くなったので、登録以外のAPIの疎通確認と処理フローの確認は次回、記載したいと思います。
最後に、失敗メモを記載しておきます。(誰かの参考になればと)
失敗メモ1
app/models/user.rb に下記が無いと、
include DeviseTokenAuth::Concerns::User
引数エラーになる。
ArgumentError (wrong number of arguments (given 1, expected 0)):
下記メソッドをoverrideしてるから。
gems/devise_token_auth-1.2.0/app/models/devise_token_auth/concerns/user.rb
# override devise method to include additional info as opts hash def send_confirmation_instructions(opts = {})
失敗メモ2
app/controllers/application_controller.rb に下記があると、
include DeviseTokenAuth::Concerns::SetUserByToken
user_signed_in?がエラーになる。current_userも。
signed_in?('user')と変えれば取れるけど、Controllerで明確に分けた方が良い。
ActionView::Template::Error (wrong number of arguments (given 1, expected 0)): 23: </button> 24: <div class="collapse navbar-collapse" id="navbarNavAltMarkup"> 25: <ul class="navbar-nav" style="margin-left: auto"> 26: <% if user_signed_in? %> 27: <li class="nav-item text-nowrap" style="height: 45px"><%= link_to (image_tag current_user.image_url(:small)).html_safe + ' ' + current_user.name, edit_user_registration_path, class: 'nav-link d-inline-block text-truncate', style: 'max-width: 400px' %></li> 28: <li class="nav-item text-nowrap"><%= link_to 'ログアウト', destroy_user_session_path, class: 'nav-link', style: 'padding-top: 10px; padding-bottom: 10px' %></li> 29: <% else %>
“Devise導入済みのアプリにDevise Token Authを入れて共存させる” に対して2件のコメントがあります。