【Rails】 devise_token_authのUnsafeRedirectError

# 概要
ポートフォリオのRailsをAPIモードで作成した際、認証機能をdevise_token_authで実装した際のエラー対応です。

## 症状
devise_token_authでメール認証を実装する場合、作成したモデルに:confirmableを追加すると思います。
このメール認証には、認証成功時のリダイレクト先を指定する必要があるのですが、本番環境で以下のエラーが発生しました。

```
ActionController::Redirecting::UnsafeRedirectError
(Unsafe redirect to "https://省略...",
pass allow_other_host: true to redirect anyway.)
```

どうやらRails7から追加された Open Redirect protection により、外部URLへのリダイレクトが弾かれるようです。
私は、Rails APIとフロント側をそれぞれ別々のホストへデプロイしていたために発生した問題となります。
エラー文中でも`pass allow_other_host: true to redirect anyway`と、外部ホストを許可しろと言われています。

https://qiita.com/kato-hiroaki/items/7d516877df4bbace45a5

## 対応策
情報があまり無かったのですが、以下のissueを参考にしました。

https://github.com/lynndylanhurley/devise_token_auth/issues/1536

どうやら、 `confirmations_controller.rb` の`show`アクションをオーバーライドして、 **`allow_other_host: true`** オプションを付与すればいいようです。
元のコードは、devise_token_authのGtHubから直接`confirmations_controller.rb`のコードを引っ張ってきました。

```api/v1/auth/confirmations_controller.rb
# ActionController::Redirecting::UnsafeRedirectError: Unsafe redirect toのエラー対応
class Api::V1::Auth::ConfirmationsController < DeviseTokenAuth::ConfirmationsController
  def show
    @resource = resource_class.confirm_by_token(resource_params[:confirmation_token])

    if @resource.errors.empty?
      yield @resource if block_given?

      redirect_header_options = { account_confirmation_success: true }

      if signed_in?(resource_name)
        token = signed_in_resource.create_token
        signed_in_resource.save!

        redirect_headers = build_redirect_headers(token.token,
                                                  token.client,
                                                  redirect_header_options)

        redirect_to_link = signed_in_resource.build_auth_url(redirect_url, redirect_headers)
      else
        redirect_to_link = DeviseTokenAuth::Url.generate(redirect_url, redirect_header_options)
      end

      # allow_other_host: trueを追加することで、外部URLへのリダイレクトを許容する
      redirect_to(redirect_to_link, allow_other_host: true)
    elsif redirect_url
      redirect_to DeviseTokenAuth::Url.generate(redirect_url, account_confirmation_success: false), allow_other_host: true
    else
      raise ActionController::RoutingError, 'Not Found'
    end
  end
end
```

ルーティングも修正します。

```config/routes.rb
Rails.application.routes.draw do
  namespace "api" do
    namespace "v1" do
      # devise_token_authのコントローラをオーバーライド
      mount_devise_token_auth_for "User", at: "auth", controllers: {
        confirmations: "api/v1/auth/confirmations",
      }
    end
  end
end
```

これで、devise_token_authのメール認証における外部URLへのリダイレクトが許可されるようになりました。

注意点として、外部URLへのリダイレクトを許容するためセキュリティリスクは高まる可能性があります。今回の実装には含めていませんが、リダイレクトの直前で、チェック処理を入れた方がいいかもしれません。


## パスワード再設定時の同様のエラー
devise_token_authでパスワードを再設定する場合も、メールからパスワード設定用ページへリダイレクトする必要があるので、そこでも **UnsafeRedirectError** が発生します。

このエラーは`passwords_controller.rb`の`edit`アクションをオーバーライドする必要があります。

```api/v1/auth/passwords_controller.rb
class Api::V1::Auth::PasswordsController < DeviseTokenAuth::PasswordsController
  def edit
    # if a user is not found, return nil
    @resource = resource_class.with_reset_password_token(resource_params[:reset_password_token])

    if @resource && @resource.reset_password_period_valid?
      token = @resource.create_token unless require_client_password_reset_token?

      # ensure that user is confirmed
      @resource.skip_confirmation! if confirmable_enabled? && !@resource.confirmed_at
      # allow user to change password once without current_password
      @resource.allow_password_change = true if recoverable_enabled?

      @resource.save!

      yield @resource if block_given?

      # allow_other_host: trueを追加することで、外部URLへのリダイレクトを許容する
      if require_client_password_reset_token?
        redirect_to DeviseTokenAuth::Url.generate(@redirect_url, reset_password_token: resource_params[:reset_password_token]),
        allow_other_host: true
      else
        if DeviseTokenAuth.cookie_enabled
          set_token_in_cookie(@resource, token)
        end

        redirect_header_options = { reset_password: true }
        redirect_headers = build_redirect_headers(token.token,
                                                  token.client,
                                                  redirect_header_options)
        redirect_to(@resource.build_auth_url(@redirect_url,
                                             redirect_headers),
                                             # allow_other_host: trueを追加することで、外部URLへのリダイレクトを許容する
                                             allow_other_host: true)
      end
    else
      render_edit_error
    end
  end
end
```

ルーティングに追加します。

```config/routes.rb
Rails.application.routes.draw do
  namespace "api" do
    namespace "v1" do
      # devise_token_authのコントローラをオーバーライド
      mount_devise_token_auth_for "User", at: "auth", controllers: {
        confirmations: "api/v1/auth/confirmations",
        passwords: "api/v1/auth/passwords",
      }
    end
  end
end
```

これで、パスワード再設定も問題無くリダイレクトされるようになりました。

以上です。