Tomo_1151
@Tomo_1151

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

Next.js 14 Route Handlers 内の Cookie の扱いについて

解決したいこと

Route Handlers内からバックエンドのAPIにfetch()でリクエストを飛ばし,JWT認証を行った上レスポンスのSet-cookieヘッダにトークンを載せて返してもらっています
このfetch()にてトークンを取得しこれをCookieに保存してクライアントコンポーネント側に返しています

この時,Route Handlers内からバックエンドのAPIに飛ばしたリクエストに対するレスポンスのヘッダにはSet-cookieaccess_token=eyJh*****と値がセットされているのは確認できるのですが,これを真下の行で cookies().get("access_token")?.value;で取得しようとすると[{ name: 'access_token', value: '' }]となってしまい,値が取得できません

自分の認識では,Set-Cookieaccess_token=eyJh****と書いてあるのを確認したらこれをCookieに保存してくれるものだと思っていたのですが,これはRoute Handlers内では別の話なのでしょうか?

また,解決策としてconst token = apiResponse.headers.get("set-cookie").split(";")[0].split("=")[1]; といったようにfetch()のレスポンスから直にSet-Cookieフィールドを取り出してcookies().set("access_token", token)した NextResponse を返すというのはおかしい(セキュリティ上の問題あり)ですか?

バックエンドのAPIはHono,フロントエンドはNext.jsで作成しています

該当するソースコード

クライアントコンポーネントからRoute Handlersへのリクエスト (一部抜粋)

const response = await fetch("/api/auth/login", {
    method: "POST",
    headers: apiConfig.headers,
    body: JSON.stringify(payload),
    credentials: "include",
});

if (!response.ok) {
    throw new Error('Internal server error', response.status);
}

const responseData = await response.json();

await login();
router.push("/");

Route Handlers からバックエンド API へのリクエスト

const apiResponse = await fetch(apiConfig.baseURL + "/auth/login", {
    method: "POST",
    headers: {
        ...apiConfig.headers
    },
    body: JSON.stringify(payload),
    credentials: "include",
});

// 正しくSet-Cookieフィールドにトークンがセットされている
console.log(apiResponse.headers.get("set-cookie"));

if (!apiResponse.ok) {
    throw new Error('Internal server error', apiResponse.status);
}

const responseData = await apiResponse.json();

// APIからのレスポンスからトークンを取り出したい

// 動く
const token = apiResponse.headers.get("set-cookie").split(";")[0].split("=")[1];

// undefined
const token = cookies().get("access_token")?.value;

cookies().set("access_token", token, {
    httpOnly: true,
    // secure: true,
    sameSite: 'lax',
    path: '/',
});
console.log(cookies().getAll());
const response = NextResponse.json({ url: "/" }, { status: 200 });
return response;

Next.js(React)初心者で色々誤解がありおかしなことを書いているかもしれませんが,おてやわらかによろしくお願いします

0

1Answer

JWT を Cookie で送信するというのは何故ですか?  自分で考えたやり方ですか? それとも Web API 側の仕様でそうせざるを得ないのでしょうか? Cookie を使うのは Cross-Site Request Forgery (CSRF) に対して脆弱だと思われるのですが。

0Like

Comments

  1. @Tomo_1151

    Questioner

    保護されたリソースにアクセスする(認証を行う)際はBearer認証を使っています

    今回の質問は,ログイン時の処理フロー中の問題で,クライアントサイドでJWTをhttpOnlyなCookieに保存するために,APIの/auth/loginのレスポンスに"Set-Cookie"を含めているがうまくセットされないといった感じです
    CSRF対策用のトークンはボディに含めやり取りする予定です
    わかりづらくてすみません

    JWTをCookieに保存することについては

    これらを参考にしています

  2. 以下はスルーしてもらって構いませんが、よろしければ返答お願いします。

    クライアントサイドでJWTをhttpOnlyなCookieに保存する

    JWT を sessionStorage や localStorage に保存すると JavaScript で JWT を取得できるのでセキュリティが低くなる。なので、JavaScript でアクセスできない HttpOnly 属性付きの Cookie に保存したいという話なのでしょうか?

    そうだとすると、

    保護されたリソースにアクセスする(認証を行う)際はBearer認証を使っています

    とする際、要求ヘッダの Authentication; Bearer ... の ... に設定する JWT をどのように取得して設定して fetch するかがチャレンジでは?

    自分が考えると以下のようになって、コードの token が JWT なのですが、それが「httpOnlyなCookieに保存」されているとすると何ともならないように思えます。

    const responseForecast = await fetch(forecastUrl,
        { headers: { 'Authorization': `Bearer ${token}` } });
    if (responseForecast.ok) {
        const data = await responseForecast.json();
        setForecasts(data);
    }
    

    そのあたりはどのように対応されるのでしょうか?

  3. @Tomo_1151

    Questioner

    色々な手法のサイトを参考にしていたために誤解をしていたのかもしれません
    Authorization: Bearer JWT みたいなものはトークンをLocalStorageに保存する前提のお話で,トークンをhttpOnlyCookieに保存する場合は,認証が必要なリソースにアクセスするときに同時にブラウザがCookieを送信するため,それをもとに認証を行うという認識であっていますか?

    そうであるとすると通信の流れは以下のようになるということでしょうか?

    1. ログイン画面からRoute HandlersへEmailやパスワードを受け流し
    2. Route HandlersからバックエンドAPIにEmailやパスワードを送信
    3. バックエンドで正当性を検証,OKであればSet-Cookieでトークンを送信
    4. Set-Cookieを受けブラウザのCookieストアに保存
    5. 今後のリクエストではCookieも同時に送信
    6. バックエンドで送信されたCookieからトークンを検証

    もし仮に上のフローで合っているとすれば,手順4.ではRoute Handlers内でバックエンドAPIからのSet-CookieヘッダはそのままResponseにセットする横流しのような形になるのでしょうか

    // バックエンドへのリクエスト
    const apiResponse = await fetch(apiConfig.baseURL + "/auth/login", {
    	method: "POST",
    	headers: {
    		...apiConfig.headers
    	},
    	body: JSON.stringify(payload),
    	credentials: "include",
    });
    
    // クライアントへのレスポンス
    const response = NextResponse.json({ url: "/" }, { status: 200 });
    
    // バックエンドからのSet-Cookieをブラウザに
    response.headers.set('set-cookie', apiResponse.headers.get('set-cookie'));
    
    return response;
    
    
  4. 上の私のコメントの質問に答えてもらえないと話が通じにくいのですが・・・

    Authorization: Bearer JWT みたいなものはトークンをLocalStorageに保存する前提のお話で,

    上のコメントで私が書いたコードの話なら、JWT の保存場所は、そこから JavaScript で取得できる場所というのが前提です。(localStorage とは限りません)

    トークンをhttpOnlyCookieに保存する場合は,認証が必要なリソースにアクセスするときに同時にブラウザがCookieを送信するため,それをもとに認証を行うという認識であっていますか?

    合っているかどうかは、実際に質問者さんのシステムが Cookie で送信される JWT を受けて認証できるかどうかにかかっていて、質問者さんのバックエンド側の実装次第ということになります。

    なので、そういう実装ができれば合っているということになると思います。できなければ合ってないということになります。できるか否かは質問者さんの実装がわからない他人には分かりません。

    その前に、当たり前のことですが、Cookie を取得した相手のドメインと、「認証が必要なリソースにアクセス」する際のアクセス先のドメインが同じでないとその話は成り立たないことは認識してますよね。

    そうであるとすると通信の流れは以下のようになるということでしょうか?
    1.ログイン画面からRoute HandlersへEmailやパスワードを受け流し
    2.Route HandlersからバックエンドAPIにEmailやパスワードを送信
    3.バックエンドで正当性を検証,OKであればSet-Cookieでトークンを送信
    4.Set-Cookieを受けブラウザのCookieストアに保存
    5.今後のリクエストではCookieも同時に送信
    6.バックエンドで送信されたCookieからトークンを検証

    質問者さんのコメントに、

    保護されたリソースにアクセスする(認証を行う)際はBearer認証を使っています

    とありましたが、それは止めて、Cookie で JWT をバックエンドに送信して認証を受けるということに方針転換したのだと理解します。

    であれば、上のコメントの 3 ~ 6 はそういうことになると思います。1, 2 については質問者さんの実装次第で、自分は分かりません。

    もし仮に上のフローで合っているとすれば,手順4.ではRoute Handlers内でバックエンドAPIからのSet-CookieヘッダはそのままResponseにセットする横流しのような形になるのでしょうか

    ブラウザから要求を出して「レスポンスのヘッダにはSet-cookieにaccess_token=eyJh*****と値がセットされているのは確認」したのですよね? すなわち、上の 3 まではできていることは確認済だと理解してます。

    「横流しのような形」って何だか分かりませんが、5 のリクエスト先のドメインが 3 の相手と同じなら何もしなくても Cookie は 5 でブラウザが自動的に要求ヘッダに含めて送信するので、質問者さんは 4, 5 では何もしなくていいです。

    ひょっとして、「レスポンスのヘッダにはSet-cookieにaccess_token=eyJh*****と値がセットされているのは確認」した相手のドメインと「5.今後のリクエストではCookieも同時に送信」する相手のドメインが異なるのですか?

    だとすると、JWT のやり取りに Cookie を使おうと考えたことがそもそも間違ってます。

  5. 質問者さん、上の通り回答したのでそれに対するフィードバックを書いてください。上の回答はスルーしないようお願いします。最低、役に立った/立たなかった、役に立たなかったならどこがダメなのかぐらいは書いてください。

  6. @Tomo_1151

    Questioner

    返信遅れて申し訳ありません

    質問者さんのコメントに、

    保護されたリソースにアクセスする(認証を行う)際はBearer認証を使っています

    とありましたが、それは止めて、Cookie で JWT をバックエンドに送信して認証を受けるということに方針転換したのだと理解します。

    そういった認識で間違いないです

    ブラウザから要求を出して「レスポンスのヘッダにはSet-cookieにaccess_token=eyJh*****と値がセットされているのは確認」したのですよね? すなわち、上の 3 まではできていることは確認済だと理解してます。

    最終的にこの質問で確認したかった部分はここです

    ブラウザ側(クライアントサイド)からはRoute Handlers(サーバーサイド)をfetch()で呼んでいるだけであり,Route Handlers内にてバックエンドAPIにfetch()を行っています

    ブラウザ(クライアントサイド)からRoute Handlersのfetch()を①,Route HandlersからバックエンドのAPIへのfetch()を②とした時に,
    実際にバックエンドとの通信を行う②ではSet-Cookieにaccess_token=eyJh*****と値がセットされているのを確認していますが,
    Route Handlersからブラウザ(クライアントサイド)へのレスポンス①では確認できていません
    (これは自分で①へのNextResponseにSet-cookieヘッダを付与しないといけない?のだと思っています)

    そのため,開発者ツール > アプリケーションからCookieの欄を見てもaccess_tokenはセットされていない状況です

    これはRoute Handlersはあくまでサーバーサイドで動くためにバックエンドからのSet-Cookieは関係ないものであり,実際のブラウザに返す①のレスポンスにバックエンドAPIから受け取ったSet-Cookieを付与して良いということで合っていますか?

    それとも違った方法でより安全に行う方法があるのでしょうか?

    /api/login/route.js
    const apiResponse = await fetch(apiConfig.baseURL + "/auth/login", {
    	method: "POST",
    	headers: {
    		...apiConfig.headers
    	},
    	body: JSON.stringify(payload),
    	credentials: "include",
    });
    
    const response = NextResponse.json({ url: "/" }, { status: 200 });
    response.headers.set('set-cookie', apiResponse.headers.get('set-cookie'))
    return response;
    
  7. 質問に答えてください。でないと話が通じません。特にこれ ↓

    ひょっとして、「レスポンスのヘッダにはSet-cookieにaccess_token=eyJh*****と値がセットされているのは確認」した相手のドメインと「5.今後のリクエストではCookieも同時に送信」する相手のドメインが異なるのですか?

  8. @Tomo_1151

    Questioner

    その前に、当たり前のことですが、Cookie を取得した相手のドメインと、「認証が必要なリソースにアクセス」する際のアクセス先のドメインが同じでないとその話は成り立たないことは認識してますよね。

    理解しています

    ひょっとして、「レスポンスのヘッダにはSet-cookieにaccess_token=eyJh*****と値がセットされているのは確認」した相手のドメインと「5.今後のリクエストではCookieも同時に送信」する相手のドメインが異なるのですか?

    同じです

  9. であれば、先に書いた通り、

    5 のリクエスト先のドメインが 3 の相手と同じなら何もしなくても Cookie は 5 でブラウザが自動的に要求ヘッダに含めて送信するので、質問者さんは 4, 5 では何もしなくていいです。

    ということです。そこは確認しましたか?

    Fiddler などのツールを使って、要求ヘッダに JWT が含まれているか調べてください。

  10. @Tomo_1151

    Questioner

    であれば、先に書いた通り、

    5 のリクエスト先のドメインが 3 の相手と同じなら何もしなくても Cookie は 5 でブラウザが自動的に要求ヘッダに含めて送信するので、質問者さんは 4, 5 では何もしなくていいです。

    ということです。そこは確認しましたか?

    確認しました
    そもそもCookieにaccess_tokenが保存されていないため送信されていません

    5 のリクエスト先のドメインが 3 の相手と同じなら何もしなくても Cookie は 5 でブラウザが自動的に要求ヘッダに含めて送信するので、質問者さんは 4, 5 では何もしなくていいです。

    バックエンドからRoute HandlersへのレスポンスにはSet-Cookieはセットされていますが,Route Handlersからクライアントサイドへの(ブラウザで確認できる)レスポンスには手動でNextResponseにset-cookieをセットしない限り,セットされません

    ブラウザが自動的にリクエストヘッダにそのドメインで保存したCookieを含めて送信することは理解しましたが,そもそもaccess_tokenはブラウザのCookieに保存されていないため送信されていません

    Route Handlersからクライアントサイドへの(ブラウザで確認できる)レスポンスには手動でNextResponseにset-cookieをセットしない限り,セットされません

    ここで,手動で以下のコードのようにセットすることが「推奨されないことであり他の方法がある」のかどうかが知りたいです

    /api/login/route.js
    const apiResponse = await fetch(apiConfig.baseURL + "/auth/login", {
    	method: "POST",
    	headers: {
    		...apiConfig.headers
    	},
    	body: JSON.stringify(payload),
    	credentials: "include",
    });
    
    const response = NextResponse.json({ url: "/" }, { status: 200 });
    response.headers.set('set-cookie', apiResponse.headers.get('set-cookie'))
    return response;
    
  11. Fiddler などのツールを使って、要求ヘッダに JWT が含まれているか調べてください。

  12. @Tomo_1151

    Questioner

    それぞれログイン時のPOST通信と認証が必要な時のGET通信です
    ログイン時にSet-Cookieは付与されておらず,認証が必要な通信でもCookieのJWTは送信されていません

    • ログイン
      image.png

    • 認証が必要な通信
      image.png

    手動でNextResponseにset-cookieをした場合です
    こちらはSet-Cookieが付与されており,認証が必要な通信ではCookieのJWTが送信されています

    • ログイン
      image.png

    • 認証が必要な通信
      image.png

  13. Route Handlers というものを理解しておらず、的外れなレスとなってしまったようですみません。

    以下のような構成となっていて、API からは Cookie が Route Handlers に送られてくるが、ブラウザには届かないので、そこを何とかしたいという話だったのでしょうか?

    ブラウザ(Client Side) ⇔ Route Handlers(Server Side) ⇔ API(Cookie 発行)

    そうだとすると、どうするのが良いかは自分には分かりません。お役に立でずすみませんが、他の方の回答をお待ちください。

  14. @Tomo_1151

    Questioner

    以下のような構成となっていて、API からは Cookie が Route Handlers に送られてくるが、ブラウザには届かないので、そこを何とかしたいという話だったのでしょうか?

    ブラウザ(Client Side) ⇔ Route Handlers(Server Side) ⇔ API(Cookie 発行)

    そういうことです
    わかりづらい質問で申し訳ありません

    お時間を割いていただきありがとうございました

Your answer might help someone💌