8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DeviseのSessionsControllerをカスタマイズ(API化)したときに苦労した件

Posted at

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化、コントローラによるルーティング処理の実装、という意味で書き始めた記事ですので、一旦はこれで終わりとします。

ありがとうございました。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?