はじめに
おはようございます。torihaziです。
現在、朝の7時前です。
昨日はdevise-jwtの入りまでで終わったので今日こそは認証を終わらせたいと思います。
技術選定
[frontend]
- Nextjs(pages router) => App Routerの理解に苦しんだため
- MUI => 調べたランキングでtopだったため
- react-hook-form => フォーム管理と言ったらこれでは?
- zod => 少しだけ使い慣れてるから
[backend]
- Rails 7 => 使い慣れているため
- devise => 定番だから
- devise-jwt => devise-token-authが古いらしいのでこっち
[github]
devise-jwt
昨日はdevise-jwtをcredentials:editで設定したところまで行ったのかな。
ということで続き。
次は何したらいいんだろうか。
user.rbにこれを追記するらしい。
これを書くことでどのモデルでjwt認証を可能にするかを設定できる。
class User < ApplicationRecord devise :database_authenticatable,・・・・ :jwt_authenticatable, jwt_revocation_strategy: Denylist => これ。 end
続いて、htmlリクエストにおいてhtmlヘッダーのAuthorizationというものを
許可しなきゃならないということなので 、initilizers/cors.rbにおいて
headers: %w(Authorization), methods: :any, expose: %w(Authorization),
で
rails generate model jwt_denylist jti:string exp:datetime rails db:migrate
しようとしたけど、
/app/app/models/user.rb:6:in `<class:User>': uninitialized constant User::Denylist (NameError) :jwt_authenticatable, jwt_revocation_strategy: Denylist
て出た。順序が逆らしい。
ということでtable作ってからやったらいけた。
なんか大変だ。これはあとでよく整理しないと。
ただ英語読めたらバッチリ書いてあるから。英語がところどころ読める感じは甘いのかな。
ええとその後にさっき作ったjwt_denylist.rbにこれを書いて
class JwtDenylist < ApplicationRecord include Devise::JWT::RevocationStrategies::Denylist self.table_name = 'jwt_denylist' end
これの名前を変えて
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist <= これね。Jwtをつけた・ end
でconfig/initializers/devise.rbに
Devise.setup do |config| # ... 他の設定 ... config.jwt do |jwt| jwt.secret = Rails.application.credentials.devise_jwt_secret_key! # ログイン(トークン発行)のリクエスト jwt.dispatch_requests = [ ['POST', %r{^/users/sign_in$}] ] # ログアウト(トークン無効化)のリクエスト jwt.revocation_requests = [ ['DELETE', %r{^/users/sign_out$}] ] jwt.expiration_time = 1.day.to_i end end
書けば、いけるのかな?
試しにリクエストを投げてみよう
docker compose up -d
であげて、
パスは/users
にPOSTで投げたらいいらしいので。
あーでもあれか。controller変えたからコントローラも新しく作らないといけないのかな・
でもどうやって認識させるんだろうか。
controllerの作り方はdeviseの方に書いてあるけど。
あれ、元々deviseでuserを新しく作るには?
デフォルトだと、/usersにpostメソッドでパラメータ添えて送ったら作られるんだよね。
なんでかっていうと/usersにpostsで送ると/devise/registrations#createに送られるから。
ということは?
何すればいいんだ。
まぁいいや。まずregistrationsのcontrollerを作るか。
いや作る必要ないのか。
やるのはあのストロングパラメータ追加したコントローラをdeviseが継承するようにってこと。
なぜそれをやらないといけないかというと、
デフォルトだと usersにpostで投げても、passwordとpassword_confirmationとかしかパラメータを受け付けないから、
追加するにはapplication_controller.rbにさらに記述をする必要がある。
そのためapplication_controller.rbに書きたいが
apiモードでやっているのでApplicationControllerはApplicationController::APIを継承している
このAPIはApplicationController::Baseの軽量版である。しかしDeviseはBaseを推奨している。
だから新しくBaseを継承したものを作る必要がある。(のかな?)
そのため新しくそのBaseを継承したcontrollerを作ってそれをdeviseに読み込ませるようにしたい。
話は戻るがdeviseはデフォルトでApplicationControllerを継承するらしい。
class DeviseController < Devise.parent_controller.constantize
の
Devise.parent_controllerとかいうのがinitilizers/devise.rbにおいてコメントアウトされている
# config.parent_controller = 'DeviseController'
だからこれをさっき新しく作ったもので置き換えて
config.parent_controller = 'Api::V1::BaseController'
こんな感じで書けば OKのはず。 (BaseControllerに名前書き直した。)
module Api module V1 class BaseController < ApplicationController::Base before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys:[:name, :email, :password, :password_confirmation]) end end end end
こうすることでdevise、DeviseControllerはもうこの子を継承するようになり、それに追随して
registrations_controller、sessions_controllerとかも変わって万事OK。であるはず。と推測。
あとはroutes.rbにおいて
Rails.application.routes.draw do namespace :api do namespace :v1 do devise_for :users end end get "up" => "rails/health#show", as: :rails_health_check end
こんな感じでよくあるふうに変えれば
/usersに投げればよかったものが/api/v1/usersになる。
これでカスタムコントローラとか作らんくて良くなったはず。
ということで役者は揃ったのでAPI開発ツールでテスト。
使うのはこれ。
https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm
一回、dockerを再起動させて、ltg。
無事にエラーとかなく立ち上がったのを確認。
緊張の瞬間。
あれ。404だ。dockerの方にもログは上がってない。
port間違えた?あーcompose.ymlで3001にしてたから?
あーそーだ。
dockerにも行っているからとりあえずリクエストとしては届いる。
Api::V1::RegitsrationControllerなんてないよ、ということだが。
ルーティングをよく理解していない?
そもそもルーティングは
だから?えーと。
元々deviseを入れると自動的に
Devise::RegistrationsController Devise::SessionsController Devise::PasswordsController
とかが使えるようになって、最初のroutingに書かれてたdevise_for :usersだと
POST /users(.:format) devise/registrations#create
ということは /api/v1/usersにpostで投げれば devise/registrations#createに処理が渡されるようにすればいい。
で今のroutingだと、
Rails.application.routes.draw do namespace :api do namespace :v1 do devise_for :users end end get "up" => "rails/health#show", as: :rails_health_check end ------------------------------------------------------------ POST /api/v1/users(.:format) api/v1/registrations#create
で向き先のcontrollerがおかしいから怒られている、と理解。
ということは /api/v1/usersにpostで投げれば devise/registrations#createに処理が渡されるようにすればいい。
これを実現するには次のように書く必要があると。
Rails.application.routes.draw do namespace :api do namespace :v1 do devise_for :users, controllers: { registrations: 'devise/registrations', # 他のDeviseコントローラーも必要に応じて } end end get "up" => "rails/health#show", as: :rails_health_check end ------------------------------------------------------------ POST /api/v1/users(.:format) devise/registrations#create
Method: ActionDispatch::Routing::Mapper#devise_for — Documentation for heartcombo/devise (main)
これでいけるのでは?
dockerをリスタートして再度リベンジ。
およ?
500error.
"exception": "#<NameError: uninitialized constant Api::V1::BaseController>",
あーbaseコントローラのファイル名がおかしかった
で再チャレンジ。
NameError (uninitialized constant ApplicationController::Base):
。。。
調べたところ APIモードでは Baseはない?みたい。
よくわかんなくなってきた。ええととりあえず、戻そう。
class ApplicationController < ActionController::API before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys:[:name, :email, :password, :password_confirmation]) end end
devise.rbのparent_controllerをコメントアウトする。
# config.parent_controller = 'Api::V1::BaseController' <= これを消す。
でルーティングはこのまま
Rails.application.routes.draw do namespace :api do namespace :v1 do devise_for :users, controllers: { registrations: 'devise/registrations', sessions: 'devise/sessions' # 他のDeviseコントローラーも必要に応じて } end end get "up" => "rails/health#show", as: :rails_health_check end
でdockerリスタート。
で投げてみるか。
ていうかActionController::APIじゃん。継承してたの。
結果は422。もういいじゃん。登録させてよ。
一旦調べ直し。
よく公式ドキュメントを最初から読んでみる。
Configuring devise for APIs · waiting-for-dev/devise-jwt Wiki · GitHub
これを中途半端にやっていたかもしれないので見直す。
最初。Responding to json
class ApplicationController < ActionController::API include ActionController::MimeResponds respond_to :json before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys:[:name, :email, :password, :password_confirmation]) end end
次。Overriding controllers。これは一旦なしで。
次。Defaulting to json as format。これは意味あるのかな?
ということで
Rails.application.routes.draw do namespace :api do namespace :v1 do devise_for :users, controllers: { registrations: 'devise/registrations', sessions: 'devise/sessions' # 他のDeviseコントローラーも必要に応じて },defaults: { format: :json } end end get "up" => "rails/health#show", as: :rails_health_check end
docker をrestartしてリクエスト投げてみる。
{ "name":"hoge1", "email":"hoge3@mail.com", "password": "password", "password_confirmation": "password", }
422だけど、なんか帰ってきた。
てかあれ、nameとか追加されてないじゃん。
てか、apiモードでrespond_toメソッド使うには、ていうのが調べたらあった。
RailsのAPIモードでrespond_to を使うにはMimeRespondsをincludeする必要がある - その辺にいるWebエンジニアの備忘録
少し進んだ。。公式ドキュメントを読み飛ばすとえらい目に遭う。
やっぱり最初に見るべきは公式ていうのは本当らしい。
少し休憩。
ただいま11:23
再開。
というかなんでこんなerrrosなんて返ってきたのか。
コードを見てみる。
# POST /resource def create build_resource(sign_up_params) resource.save yield resource if block_given? if resource.persisted? if resource.active_for_authentication? set_flash_message! :notice, :signed_up sign_up(resource_name, resource) respond_with resource, location: after_sign_up_path_for(resource) else set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}" expire_data_after_sign_in! respond_with resource, location: after_inactive_sign_up_path_for(resource) end else clean_up_passwords resource set_minimum_password_length respond_with resource end end
ここに処理が渡っていることは確か。
resourceはActiveRecordオブジェクトだと思うから、persisted?で保存できてないのでそこの
ifから飛んで結局はrespond_with resourceか。
でそのActiveRecordオブジェクトのバリデーションエラーががrespond_withされただけか。
sign_up_paramsを覗きに行きたい。
でこうするにはカスタムコントローラを作るしかない。
ということで作る。
rails generate devise:controllers [scope]
rails generate devise:controllers users -c registrations
で。
controllers/users/registrations_controller.rbができるから。
class Api::V1::Users::RegistrationsController < Devise::RegistrationsController before_action :configure_sign_up_params, only: [:create] ~~ def create super end private def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up, keys:[:name, :email, :password, :password_confirmation]) end ~~~ end
てしたら?ダメだ。
なぜ?
というかこのsign_up_paramsをparams.require[:user].permitてすればいけると思うんだけどどうだろう。
うん。やっぱりいけた。
でもそしたらなんで公式ドキュメントのものがダメだったんじゃ。
あと jti の notnullviolationが出てる。
詰み。
今、14:14。
現実逃避してた。今、14:48。
もう一回見直してみる。
DenylistでやっていたのにJTIMathcerで指定されていたUserモデルにjtiカラムを追加してたから
nullViolationが出ていたと思う。ということでdenylistでやり直す。
rollbackして最初から。
migration user、jwt_denylistのファイル
# frozen_string_literal: true class DeviseCreateUsers < ActiveRecord::Migration[7.1] def change create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at ## 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 ## Lockable # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at t.string :name, null: false t.timestamps null: false end add_index :users, :email, unique: true add_index :users, :reset_password_token, unique: true add_index :users, :confirmation_token, unique: true # add_index :users, :unlock_token, unique: true end end
class CreateJwtDenylists < ActiveRecord::Migration[7.1] def change create_table :jwt_denylist do |t| t.string :jti, null: false t.datetime :exp, null: false end add_index :jwt_denylist, :jti end end
終わったので rails db:migrate
class JwtDenylist < ApplicationRecord include Devise::JWT::RevocationStrategies::Denylist self.table_name = 'jwt_denylist' end
書いて、user.rbに
class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist end
でいいのかな。
ていうか今頃気づいたけど、このカスタムコントローラ作った時にあった
def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :email, :password, :password_confirmation]) end
これってあってるの?configure_sign_up_paramsなんて公式に書いてないと思う。
もういいや。これで設定できないんじゃね?知らんけど。
def sign_up_params params.require[:user].permit(:name, :email, :password,:password_confirmation) end
でいいや。
で次。
いろいろ右往左往して。登録はできたけど、セッション関連で怒られる。
登録ができるまでの道筋
このまま書き続けると永遠に終わらなさそうなので、この形式にする。
現状はroutes.rbは
namespace :api, defaults: {format: :json} do namespace :v1 do devise_for :users, controllers: { registrations: 'api/v1/users/registrations' } end end
にして、api/v1/users/registrations_controller.rbに
class Api::V1::Users::RegistrationsController < Devise::RegistrationsController def create super end def sign_up_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end
にして
これで投げたら、
ActionDispatch::Request::Session::DisabledSessionError: Your application has sessions disabled. To write to the session you must first configure a session store>
みたいな感じになった。
で、rails cしてみると、User自体は登録はできているらしい。
ただ、セッション周りのエラーが起きている、という状況。
DeviseはWardenに依存し、Wardenはセッションを必要とする。
で今、apiモードっていうセッション使わないモードでセッション使おうとしてるからエラーになってるらしい。
ということでこれやってみる。
https://github.com/heartcombo/devise/issues/5443
controllers/concerns/rack_session_fix.rbという名前で
module RackSessionFix extend ActiveSupport::Concern class FakeRackSession < Hash def enabled? false end end included do before_action :set_fake_rack_session_for_devise end private def set_fake_rack_session_for_devise request.env['rack.session'] ||= FakeRackSession.new end end
でregistrations_controller.rbに
class Api::V1::Users::RegistrationsController < Devise::RegistrationsController include RackSessionFix ~~
こんな感じで追加。
コンテナリスタートして再チャレンジ。
やったーーーー~~~~~~~~~~~~~~~~~~~~~~~~ーーーーーーーーー。
ただいま 16:42。
でもどうして responseヘッダにAuthorizationのアレが出るんだろう。
まぁいいや。
もう疲れた。しかもauthorizationて小文字だし。
ということで、登録はできたので次はログイン。
ログインは普通にログインすればいいのかな。
でも疲れたから、一旦運動してこよ。
今、16:54。
かいし.18:16。
rails g devise:controllers api/v1/users -c sessions
これでもいけるっぽい。直接api/v1/users下に作られる。
class Api::V1::Users::SessionsController < Devise::SessionsController include RackSessionFix ~~~ # POST /resource/sign_in def create super end def sign_in_params params.permit(:user).permit(:email, :password) end end
こんな感じ。
これでいけるかな?
401のunauthorizedだ。
あれか。認証メール見てないからか?
確認メールの設定をするためにletter_openerを入れる。
gem letter_opener を試してみる #Rails - Qiita
これで入れた。
ということで、この状態でユーザ登録して確認メールがいけばおk
でこのconfirm とかいう下のリンククリックして、
うまく行くはずだがいかず。
試しにconsoleからuserのconfirmed_atにTime.nowを入れてみる
そしてログインしたらうまく行くんでは?
だめ。もー。なんでよー。
ただいま現実逃避中」。繰り返す。現実逃避中。
21:14。
{ "error": "You need to sign in or sign up before continuing." }
これ出るんだけどなんあん。
unauthenticatedということらしい。devise_en.ymlに書いてあった。
んー。なんかdeviseで詰まってね。jwt云々の前に。
あれ、こんな詰まったっけ。昔触ったことあったんだけど。
終わりに
あら、終わらんかった。
deviseの理解が酷い、ということに気づいた。
なんでストロングパラメータ機能しなかったんだろ。
ま、いいや。