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

DeviseのサインインコントローラをAPI化するにあたって苦労したので、まとめました。

非常に個人的、個別ケースに関する記事となっておりますが、謎のwarden.authenticate!401に悩まされている方が散見されたため＆解決方法が不明瞭のため、サンプルの一つとして共有させていただきます。

解決できる課題： 認証失敗時の処理の実装、warden.authenticate!の401エラー回避

deviseのカスタムコントローラを生成する方法、ルーティングetc..については公式のreadmeや他のまとめ記事が充実しているため省略。


```ruby:デフォルトのコントローラ
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化していきます。

```ruby:デフォルトのカスタムコントローラ
class Api::V1::Users::SessionsController < Devise::SessionsController
  def create
    super
  end
end
```

まずはコピペ。
そして、blockを受け付ける予定はないのでyieldを削除、かつログイン後のルーティングはjson形式で返すのでrespond_withを書き換え。
json形式のデータについては、採用しているgemなどによるのでカスタマイズしてください。

```ruby:
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](https://github.com/jsonapi-serializer/jsonapi-serializer)を用いているので、

```ruby:
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のデフォルトコントローラに定義されていて、直接はさわれないので、オーバーライドしてやります。

```ruby:
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の値を書き換えます。

```ruby:
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
```
([↑参考記事、というかほぼ丸パ○リ](https://qiita.com/eccyan/items/b064bcd1e6f740ee5472))

失敗時のレスポンスにdeviseのエラーメッセージを渡したい場合はflash[:alert]に格納されているので、そちらをご利用ください。

```ruby:例
json_response = {message: flash[:alert]}.to_json
```


最後にauth_optionsのresource_nameを書き換えます。

```ruby:
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の値を読み取るようです。

具体的には、次のようにリクエストしていると

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

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