1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Next.js/Rails】Next.js、RailsでCookieによるログイン認証を行う時にハマったこと

Last updated at Posted at 2024-11-04

はじめに

フロントエンドにNext.js、バックエンドにRailsを使用した構成で、Cookieを使った認証時に発生した問題とその解決方法をメモとしてまとめました!

結論

結論を先に言うと
Next.jsのApp Routerを使用した場合、サーバーサイド(Rails)へのリクエストはNode.jsから送信されるため、クライアント側で保持されているCookieが正しく含まれないことがあります
これを解決するためには、cookies()関数など使用してサーバーサイドでCookieを取得し、リクエストヘッダーに設定する必要がありました

問題

  • フロントエンドでログイン後、データを取得しようとすると、認証エラー(401 Unauthorized)が返却される
  • Postmanでは正常に動作する
  • RSpecでの認証に関するテストはパスしている

サーバー側の処理

関係箇所のみ抜粋(Deviseなどのgemは使用していません)

class SessionsController < ApplicationController
  skip_before_action :authenticate_user!, only: %i[create]

  def create
    user = User.find_by(account_name: session_params[:account_name])
    if user&.authenticate(session_params[:password])
      session[:user_id] = user.id
      head :ok
    else
      render json: { message: 'アカウント名とパスワードの組み合わせが不正です' }, status: :unauthorized
    end
  end

  def destroy
    reset_session
    render json: { message: 'ログアウトしました' }, status: :ok
  end

  private

  def session_params
    params.require(:session).permit(:account_name, :password)
  end
end

このコードでは、アカウント名とパスワードを受け取り、認証に成功すればセッションにuser_idを保存し、Cookieを返すようになっています

class ApplicationController < ActionController::API
  include ActionController::Cookies
  before_action :authenticate_user!

  private

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  def authenticate_user!
    render_unauthorized_error('ログインしてください') unless current_user
  end
end

共通の前処理として:authenticate_user!を設定し、current_userメソッドで認証確認を行なっています!

原因調査

現状、ログイン後にメモの一覧取得APIを叩くと、401 Unauthorizedエラーが返ってくることが分かっています
まずは、サーバー側のログを見てみます!

サーバー側のログ確認

Started GET /memos for XXX.XXX.XX.X at 2024-11-02 15:47:39 +0900
Processing by MemosController#index as */*
  Parameters: {memo=>{}}
  [1m[36mUser Load (44.0ms)[0m  [1m[34mSELECT `users`.* FROM `users` WHERE `users`.`id` IS NULL LIMIT 1[0m
  ↳ app/controllers/application_controller.rb:9:in `current_user
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 104ms (Views: 3.8ms | ActiveRecord: 50.7ms (1 query, 0 cached) | GC: 0.0ms)

ユーザーIDがNULLとなっているため、認証エラーが発生しているみたいです
フロント側から渡ってきたCookieが空である可能性がありそうです

Cookieの確認

current_userメソッドの前にリクエストヘッダーのCookieをログに出力して確認してみます

def authenticate_user!
  logger.debug "Cookie Header: #{request.headers['Cookie']}"
  render_unauthorized_error('ログインしてください') unless current_user
end

結果:

Cookie Header:

リクエストヘッダーのCookieはやっぱり空でした
ここから2つの可能性が考えられそうです

  • ログイン時にサーバー側できちんとCookieを返していない
  • ブラウザ側でCookieが保持されていない

サーバーでのCookieの返却を確認

ログイン時にCookieが返されているかを確認するため、またログを出力するようにします!
愚直にベタベタ書いてきます笑

def create
  user = User.find_by(account_name: session_params[:account_name])
  if user&.authenticate(session_params[:password])
    session[:user_id] = user.id
    logger.debug "ログイン時:バックエンドのクッキー"
    cookies.each { |key, value| logger.debug "Cookie #{key}: #{value}" }
    logger.debug "ログイン時:バックエンドのセッション"
    session.each { |key, value| logger.debug "Session #{key}: #{value}" }
    head :ok
  else
    render json: { message: 'アカウント名とパスワードの組み合わせが不正です' }, status: :unauthorized
  end
end

結果:

ログイン時:バックエンドのクッキー
Cookie_Header: _progaku_archive_session=...
ログイン時:バックエンドのセッション
Session session_id: ...
Session user_id: 3

サーバー側で正しくCookieとセッションが生成されていることは確認できました!

ブラウザにCookieが保持されているか?

ブラウザの検証ツールで確認したところ、ログイン直後はCookieが保持されていることも確認できました
image.png

注意
SameSiteをnoneにしてクロスドメインでのリクエスト許可しているのにもかかわらずSecureをTrueにしていませんが開発環境だからです。本番ではSecureをTrueにしてHttps通信時のみCookieを使用するように制限してください!

どういうこと!?

この辺りから、思考が停止し始めました笑
サーバー側でCookie、Sessionも問題なく生成され、ブラウザ側にも保持されている
にも関わらずリクエスト時には渡ってこない
何故??

念の為Fetch APIの設定確認

const response = await fetch(`${process.env.NEXT_PUBLIC_API}/memos`, {
  method: 'GET',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
  },
});

credentials: 'include' を設定してCookieを受け付け、リクエストに含める設定にはなっています。

ちなみに include はクロスオリジン間でもCookieなどの資格情報を含めるオプションです。
 

Postmanの動作確認

Cookie Header: _progaku_archive_session=eZLeQLjfX2urWEA2mdVw3%2Bh...(以下省略)

やはりPostmanでは問題なくCookieが渡ってきてmemoも取得できてしまいました

リロードでCookieが消える問題

検証ツールで色々試していると、ログイン後、リロードするとCookieが消えてしまうことに気づきました
Cookieの期限が適切にヘッダーに含まれてない?それともCORS周りの設定が起因している?
わからなかったので、とりあえずHTTPの全てのHeaderをログに出力して
ログイン時と、メモ取得時とで比較して確認することにしました。

ログイン時とメモ取得時のHeaderログ比較

各処理内に下記のコードを追加

      request.headers.each do |key, value|
        # ヘッダーのキーが HTTP_ で始まるものだけを出力する
        if key.start_with?('HTTP_') || key == 'CONTENT_TYPE' || key == 'CONTENT_LENGTH'
          logger.debug "#{key}: #{value}"
        end
      end

ログイン時

=== Request Headers ===
HTTP_HOST: localhost:8080
HTTP_CONNECTION: keep-alive
CONTENT_LENGTH: 79
HTTP_SEC_CH_UA_PLATFORM: "macOS"
HTTP_USER_AGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
HTTP_SEC_CH_UA: "Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"
CONTENT_TYPE: application/json
HTTP_SEC_CH_UA_MOBILE: ?0
HTTP_ACCEPT: */*
HTTP_ORIGIN: http://localhost:3000
HTTP_SEC_FETCH_SITE: cross-site
HTTP_SEC_FETCH_MODE: cors
HTTP_SEC_FETCH_DEST: empty
HTTP_REFERER: http://localhost:3000/
HTTP_ACCEPT_ENCODING: gzip, deflate, br, zstd
HTTP_ACCEPT_LANGUAGE: ja,en-US;q=0.9,en;q=0.8
HTTP_COOKIE: _progaku_archive_session=96jfLN3gVfkjxnpaC%2FpJGNqYL4CvOz8v5CcfL6CP67St4Pa8DoYumNXxt43%2B%2B%2FnNNCvmQt%2BYtUEEOBXb3YH0JIJ36Z51JVvkymsokFvGTaWJjtLmqr%2F%2F3dhTqNmXrWW2evweRmd1HwrcrsdT2Cp%2B6tgpPqnoiLt2hv2FRWBsYPdImqtwOW0PG5Mf4T9j8IFSo7SGVGGwMyJLGGxak89uyg%3D%3D--DAQiHiIvY%2FOLbWuw--swdD3Hnyfd04UtqhM%2BkFlQ%3D%3D
HTTP_VERSION: HTTP/1.1
========================

メモ取得時

=== Request Headers ===
HTTP_HOST: localhost:8080
HTTP_CONNECTION: keep-alive
CONTENT_TYPE: application/json
HTTP_ACCEPT: */*
HTTP_ACCEPT_LANGUAGE: *
HTTP_SEC_FETCH_MODE: cors
HTTP_USER_AGENT: node
HTTP_ACCEPT_ENCODING: br, gzip, deflate
HTTP_VERSION: HTTP/1.1
========================

ここであることに気づきました

User-Agentが違う

User-Agentは簡単にいうとクライアントの情報になります

ログイン時は、ブラウザからリクエストが送られています

HTTP_USER_AGENT: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36

でもメモ取得時は

HTTP_USER_AGENT: node

nodeとなっていました

User-Agentのnodeって?

Node.jsのことのようです
何故とこうなるのか調べると
Next.jsでApp Routerを使用するとサーバーサイドでレンダリングが行われ、Node.jsがリクエストを送信される仕様になっているようです

解決方法

credentials: 'include',

この設定だけでは、サーバーサイド(Node)側ではブラウザのCookieを参照できないため、Cookieを含めてのリクエストが正常に行われていなかったということだと考えられます(間違っていたらすみません)

そのためCookieのヘッダーを明示的に指定してあげる必要があるようです。

NextでブラウザのCookieを取得する関数は下記に載っていたので

このようにCookie関数で取得して、設定するようにしました。

export async function getMemos() {
	const cookieHeader = cookies().toString();
	const response = await fetch(`${process.env.NEXT_PUBLIC_API}/memos`, {
		method: 'GET',
		credentials: 'include',
		headers: {
			'Content-Type': 'application/json',
			Cookie: cookieHeader,
		},
	});

これで再度実行すると

Started GET "/memos" for XXX.XXX.XX.X at 2024-11-03 16:42:28 +0900
Processing by MemosController#index as */*
  Parameters: {"memo"=>{}}
=== Request Headers ===
User-Agent: node
HTTP_HOST: localhost:8080
HTTP_CONNECTION: keep-alive
CONTENT_TYPE: application/json
HTTP_COOKIE: _progaku_archive_session=eZLeQLjfX2urWEA2mdVw3%2BhUAPykM%2B5NpC6%2B2efjdpisVzXAgwLJfPzgbPE0AXRntsZ%2B53ZM7uQIrKLVljKPZbeQ7YU%2FapsCFpKU8E1QJ5K9cN0w0pG0MyE3HRRqSo%2BZ4JYzdOX7L%2F1ToOQrsphUDsx0aMx62PGiUIX7D%2FaA9wiXXfbNxMf8sc90jY8%2F%2BvIoodOUXFWXLp55dxcc3a2f4A%3D%3D--c2hJS4hYIkKBFprJ--YpkXKIGNqrAC365ERRV5bQ%3D%3D
HTTP_ACCEPT: */*
HTTP_ACCEPT_LANGUAGE: *
HTTP_SEC_FETCH_MODE: cors
HTTP_USER_AGENT: node
HTTP_ACCEPT_ENCODING: br, gzip, deflate
HTTP_VERSION: HTTP/1.1
========================
  User Load (1.0ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1
  ↳ app/controllers/application_controller.rb:33:in `current_user'
  Memo Count (2.4ms)  SELECT COUNT(*) FROM `memos`
  ↳ app/models/memo.rb:39:in `call'
  Rendering memos/index.json.jbuilder
  Memo Load (0.6ms)  SELECT `memos`.* FROM `memos` ORDER BY `memos`.`id` DESC LIMIT 10
  ↳ app/views/memos/index.json.jbuilder:3
  MemoTag Load (1.1ms)  SELECT `memo_tags`.* FROM `memo_tags` WHERE `memo_tags`.`memo_id` IN (5840, 5839, 5838, 5837, 5836, 5835, 5834, 5833, 5832, 5831)
  ↳ app/views/memos/index.json.jbuilder:3
  Tag Load (0.4ms)  SELECT `tags`.* FROM `tags` WHERE `tags`.`id` = 233
  ↳ app/views/memos/index.json.jbuilder:3
  Rendered memos/index.json.jbuilder (Duration: 6.5ms | GC: 1.2ms)
Completed 200 OK in 13ms (Views: 4.7ms | ActiveRecord: 5.5ms (5 queries, 0 cached) | GC: 1.2ms)

無事にCookieによるログイン認証を行い、メモを取得することができました!

まとめ

今回の問題では、Next.jsのApp Routerを使用する際、サーバーサイドでリクエストを送信するために、ブラウザのCookieが正しく含まれずに認証エラーが発生することがわかりました!
そして、この時のHeaderを見た感じ、CORS関連のHEADERが付与されてないので、POSTやDELETEした時CORSエラーが発生しそうですね・・・
その辺の設定もまたする必要がありそうです!
フレームワークの内部仕様など理解した上で使うことの大切さを身を持って感じました。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?