providerが”email”の場合、uidにメールアドレスが入り、変更時にuidも更新されます。
uidは認証に使っているので、メールアドレス変更(メールアドレス確認のURLにアクセスした)直後にフロントがログイン状態だとuid変更に気付けないので、認証に失敗します。
確認のURLにパラメータ渡せたと思いますが、パラメータで渡したくないのと、別端末や別ブラウザだと通知できないので、uidが不変の値になるようにidに変更しました。
また、ローカルストレージ(Vuexストアも)に個人情報(今回はメールアドレス)を入れていますが、極力、保持したくないので、レスポンスから外して、登録情報詳細APIを別途、作成しました。
方針
DBのuidの値は変更したくない。運用後に変更する場合に手間が大きい。
override出来ないかGemのソースを追いましたが、影響範囲が大きいので、prepend_before_actionとafter_action使って、リクエストの最初と最後にヘッダを書き換える事にしました。
idは個人情報(個人を特定できるのでは?)との疑問に思うかもしれませんが、そもそもidはAPIでは渡さない方が良い。
将来のリプレイスで、idに依存して制約が増えてしまうので、あくまでもバックエンド(Rails)の中だけに留めておいた方が良く、識別が必要な場合はユニークなkeyやcodeを用意する。
一時的だったり、変わっても良いものは、無理せずidでもOKかと。
実装修正
app/controllers/application_controller.rb を変更
class ApplicationController < ActionController::Base + after_action :update_response_uid_header private + # リクエストのuidヘッダを[id+36**2](36進数)からuidに変更 # Tips: uidがメールアドレスだと、メールアドレス確認後に認証に失敗する為 + def update_request_uid_header + return if request.headers['uid'].blank? + + user = User.find_by(id: request.headers['uid'].to_i(36) - (36**2)) + request.headers['uid'] = user&.uid + end + + # レスポンスのuidヘッダをuidから[id+36**2](36進数)に変更 + def update_response_uid_header + return if response.headers['uid'].blank? + + user = User.find_by(uid: response.headers['uid']) + response.headers['uid'] = user.present? ? (user.id + (36**2)).to_s(36) : nil + end
暗号化も考えましたが、意味のない数字にそこまでしてもと思ったので、36進数に変換しました。
> 1.to_s(36) => "1" > 10.to_s(36) => "a" > 35.to_s(36) => "z"
35まで1桁なのも微妙なので、36**2を加算して、3桁以上になるようにしました。
> (1 + 36**2).to_s(36) => "101" > (10 + 36**2).to_s(36) => "10a" > (35 + 36**2).to_s(36) => "10z"
app/controllers/application_auth_controller.rb を変更
(Devise Token Auth以外のAPIの継承元)
class ApplicationAuthController < ApplicationController include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token, if: :format_api? prepend_before_action :not_acceptable_response_not_api_accept, if: :format_api? + prepend_before_action :update_request_uid_header before_action :standard_devise_support
app/controllers/users/auth/sessions_controller.rb を変更
class Users::Auth::SessionsController < DeviseTokenAuth::SessionsController include DeviseTokenAuth::Concerns::SetUserByToken skip_before_action :verify_authenticity_token prepend_before_action :unauthenticated_response_sign_out, only: %i[destroy], unless: :user_signed_in? prepend_before_action :not_acceptable_response_not_api_accept + prepend_before_action :update_request_uid_header
他のprepend_before_actionよりも先に実行する為、prepend_before_actionの最後に追加する。
最初に追加するとuser_signed_in?が常にfalseになってしまいます。
理由は、prepend_before_actionがbefore_actionリストの先頭に追加される為で、後に記載した方が先に実行されるから。
ApplicationControllerで1回だけ定義にしたかったのですが、継承元の方が先にリストに入るので、個別に定義するようにしました。
after_actionは後ろに追加で、厳密には最後ではないですが、他のactionに影響しないので、ApplicationControllerで1回だけ定義しています。
同様に下記にも追加
app/controllers/users/auth/confirmations_controller.rb
app/controllers/users/auth/passwords_controller.rb
app/controllers/users/auth/registrations_controller.rb
app/controllers/users/auth/token_validations_controller.rb
app/controllers/users/auth/unlocks_controller.rb
Spec修正
※作成済みのSpecに依存した差分になっていますので、参考までに。
spec/support/user_contexts.rb を変更
shared_context 'APIログイン処理' do |target = :user, use_image = false| include_context 'ユーザー作成', target, use_image let(:auth_token) { user.create_new_auth_token } let(:auth_headers) do { - 'uid' => auth_token['uid'], + 'uid' => (user.id + (36**2)).to_s(36), 'client' => auth_token['client'], 'access-token' => auth_token['access-token'] } end end
let(:expect_exist_auth_header) do - expect(response.header['uid']).to eq(current_user.email) + expect(response.header['uid']).to eq((current_user.id + (36**2)).to_s(36)) expect(response.header['client']).not_to be_nil expect(response.header['access-token']).not_to be_nil # Tips: 一定時間内のリクエスト(batch_request)は半角スペースが入る expect(response.header['expiry']).not_to be_nil # Tips: 同上 end
今回のコミット内容
※追加実装した登録情報詳細API(TODO: パスワード認証追加)も含まれています。
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/e7b79abc264177a5cf90155ecac0b314220adfa3