データ移行せずに、登録済みのユーザがそのまま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件のコメントがあります。