device_token_authのコードリーディング

実務でdevice_token_authを扱うことになりました。
理解を深めるために、ログイン認証時に実行されるsessions_controllerのcreateアクションをコードリーディングしてみました。

## sessions_controllerのcreateアクションとは?
ログイン認証時に実行されるアクションです。
createアクションの処理のはざっくり以下のような流れで構成されています。

1. 認証対象のレコードの検索
2. 認証処理
    1. 認証対象のレコードが存在し、認証可能な状態にある場合
    2. 認証対象のレコードが存在するが、認証可能な状態にない場合
    3. 1と2いずれにも当てはまらない場合

※createアクション内で呼び出される他controllerや他moduleの処理は深追いして解説はしません。

## 1. 認証対象のレコードの検索

```ruby
      if field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
        q_value = get_case_insensitive_field_from_resource_params(field)

        @resource = find_resource(field, q_value)
      end
```
`field`, `q_value`にはそれぞれ以下の値が設定されます。そして、これらの変数を引数に`find_resource`を呼び出して、認証対象のレコードを検索します。

 - **field：認証用のカラム(例：email, login_id)**
resource_params(認証に関するリクエストパラメータ)のキーとdeviceで設定したauthentication_keys(認証用のカラム名)の重複値が格納される。
- **qvalue：resource_paramsのうち、`field`のキーに対するvalue**
この時valueをそのまま格納するわけではなく、deviceの設定で`case_insensitive_keys`(大文字小文字を区別しない)もしくは`strip_whitespace_keys`(空白を無視する) に`field`のキーが指定されている場合は、適切な加工を実施したうえで格納される。

## 2. 認証処理
認証処理は3つのパターンに分かれます。

### 1. 認証対象のレコードが存在し、かつそれが認証可能な状態にある場合
認証可能な状態とは、アカウントがロックされておらず、かつメール認証等のアカウント認証も完了している状態を指します。
```ruby
      if @resource && valid_params?(field, q_value) && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
        valid_password = @resource.valid_password?(resource_params[:password])
        if (@resource.respond_to?(:valid_for_authentication?) && !@resource.valid_for_authentication? { valid_password }) || !valid_password
          return render_create_error_bad_credentials
        end

        create_and_assign_token

        sign_in(@resource, scope: :user, store: false, bypass: false)

        yield @resource if block_given?

        render_create_success
```


冒頭の条件部分にて`valid_params?(field, q_value)`はリクエストパラメータにpasswordキーが存在することをチェックします。

`valid_password `にはパスワードの認証結果がbooleanで保持されます。

パスワード認証に失敗した場合は、認証情報が正しくない旨のエラーメッセージを返却します。

認証に成功した場合は、`create_and_assign_token`でトークンを生成し、ログイン処理を実行します。
最後に`render_create_success`でレスポンスとして、トークン情報や有効期限等をクライアントに返却します。

:::note info
補足
**valid_for_authentication**や後述の**active_for_authentication**はざっくり「そのアカウントがロックされておらず、かつメール認証等のアカウント認証も完了しているか」を判定するインスタンスメソッドと解釈ください。いずれもlib/devise/models/authenticatable.rbで定義されたメソッドですが、lockableやconfirmableでオーバーライドされており、ブラックボックスな形になっています。
:::



### 2. 認証対象のレコードが存在するが、認証可能な状態にない場合
対象のレコードは存在するが、ロックされている、メールでのアカウント認証が済んでいない等認証可能な状態になっていない場合の処理です。
```ruby
      elsif @resource && !Devise.paranoid && !(!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
        if @resource.respond_to?(:locked_at) && @resource.locked_at
          render_create_error_account_locked
        else
          render_create_error_not_confirmed
        end
```

ロックされている場合はその旨のエラーを、それ以外の場合はアカウントが未認証である旨のエラーを返却します。

※`!Devise.paranoid`はDeviseの設定でparanoidモードが無効になっていることを指します。説明を簡略化するために省きましたが、レコードが存在しそれが認証可能な状態にない場合でも、paranoidモードが有効な場合は3の分岐に入ります。

### 3. 1と2いずれにも当てはまらない場合
```ruby
      else
        hash_password_in_paranoid_mode
        render_create_error_bad_credentials
      end
```

認証情報が正しくない旨のエラーメッセージを返却します。
`hash_password_in_paranoid_mode`はparanoidモードが有効な場合に実行されるセキュリティ対策(タイミング攻撃への対策)の処理だそうです。(paranoidモードについては詳しくないため、別記事にまとめたいと思います。)

## 今後
createアクションの大まかな流れは以上です。今後createアクション内で呼び出されている処理も気が向いたら読んでいきたいと思います。