DeviseのサインインコントローラをAPI化するにあたって苦労したので、まとめました。
非常に個人的、個別ケースに関する記事となっておりますが、謎のwarden.authenticate!401に悩まされている方が散見されたため&解決方法が不明瞭のため、サンプルの一つとして共有させていただきます。
解決できる課題: 認証失敗時の処理の実装、warden.authenticate!の401エラー回避
deviseのカスタムコントローラを生成する方法、ルーティングetc..については公式のreadmeや他のまとめ記事が充実しているため省略。
class Devise::SessionsController < DeviseController
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
yield resource if block_given?
respond_with resource, location: after_sign_in_path_for(resource)
end
end
こちらをAPI化していきます。
class Api::V1::Users::SessionsController < Devise::SessionsController
def create
super
end
end
まずはコピペ。
そして、blockを受け付ける予定はないのでyieldを削除、かつログイン後のルーティングはjson形式で返すのでrespond_withを書き換え。
json形式のデータについては、採用しているgemなどによるのでカスタマイズしてください。
class Api::V1::Users::SessionsController < Devise::SessionsController
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
json_response = "json形式のレスポンスデータを生成"
render :json
end
end
ちなみに私はfast JSON:APIの事実上の後継であるjson-api-serializerを用いているので、
session_response = SessionResponse.new(status: 'success', message: "signed in as #{resource.nickname}", user_id: resource.id)
json_response = SessionResponseSerializer.new(session_response)
のようにしました。(2つのSession〜クラスは別途定義)
さて、第一の課題はエラー発生時のハンドリング。
デフォルトだとviewファイルをレンダリングする仕様なので、jsonを返すように変換してやる必要があります。
2行目のwarden.authenticate!(auth_options)
が鍵でして、ログイン認証に失敗すると、自動でsessionコントローラ内の別のアクションにリダイレクトします。
デフォルトだとnewに飛ぶように設定されています。
書き換えるにはauth_optionsの設定値を書き換えてやる必要があります。
auth_options自体はdeviseのデフォルトコントローラに定義されていて、直接はさわれないので、オーバーライドしてやります。
class Api::V1::Users::SessionsController < Devise::SessionsController
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
json_response = "json形式のレスポンスデータを生成"
render :json
end
protected
def auth_options
{ scope: resource_name, recall: "#{controller_path}#new" }
end
end
ここではfailアクションを用意して、認証失敗時にはそちらにリダイレクトするように変更してやります。
アクションを用意すると同時にauth_optionsの値を書き換えます。
class Api::V1::Users::SessionsController < Devise::SessionsController
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
json_response = "成功時のjson形式のレスポンスデータを生成"
render :json
end
def fail
json_response = "失敗時のjson形式のレスポンスデータを生成"
render :json
end
protected
def auth_options
{ scope: resource_name, recall: "#{controller_path}#fail" }
end
end
失敗時のレスポンスにdeviseのエラーメッセージを渡したい場合はflash[:alert]に格納されているので、そちらをご利用ください。
json_response = {message: flash[:alert]}.to_json
最後にauth_optionsのresource_nameを書き換えます。
class Api::V1::Users::SessionsController < Devise::SessionsController
def new
json_response = "失敗時のjson形式のレスポンスデータを生成"
render :json
end
def create
self.resource = warden.authenticate!(auth_options)
set_flash_message!(:notice, :signed_in)
sign_in(resource_name, resource)
json_response = "成功時のjson形式のレスポンスデータを生成"
render :json
end
protected
def auth_options
{ scope: :user, recall: "#{controller_path}#new" }
end
end
resource_nameメソッドの返り値はコントローラの名前空間をスネークケース&シンボル化&単数化したものです。
今回の場合はApi::V1::Users::SessionsController
なので:api_v1_user
が返ります。
wardenをレシーバとして呼ばれるauthenticate!メソッドは、scopeの値によりparamsの値を読み取るようです。
具体的には、次のようにリクエストしていると
params
=> <ActionController::Parameters {"user"=>{"email"=>"example@mail.com", "password"=>"xxxxxx"}, "controller"=>"api/v1/users/sessions", "action"=>"create" permitted: false>
元のresource_nameのままではuserの値である認証データ{"email"=>"example@mail.com", "password"=>"xxxxxx"}
を読み取ってくれないため、Completed 401 Unauthorized
となります。
(エラーメッセージは"Invalid Email or password."
)
今回、セキュリティ面の対応はしていませんが、アプリリリースにあたってはパスワードを保護したり、CSRF対策のトークンの生成をしたりetc..追加の変更が必要かと思われます。
今回は、API化、コントローラによるルーティング処理の実装、という意味で書き始めた記事ですので、一旦はこれで終わりとします。
ありがとうございました。