質問するログイン新規登録
Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

Q&A

解決済

1回答

1432閲覧

CSRF対策をした認証の実装エラー(react/laravel breeze api)

ryuichi-works

総合スコア40

Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

React.js

Reactは、アプリケーションのインターフェースを構築するためのオープンソースJavaScriptライブラリです。

0グッド

0クリップ

投稿2023/08/22 18:09

編集2023/08/23 09:47

0

0

実現したいこと

ここに実現したいことを箇条書きで書いてください。

  • フロントをReact、バックエンドapiをlaravelのlaravel breezeでuserとadminのマルチログイン認証を実装したい。
    目的:apiでのアプリ作成の理解のため、簡易的に作成中。

前提

csrfトークンによるcsrf対策をした認証システム作っています。
バックエンドのlaravelの方はapiテストツールのpostmanでlogin,logoutリクエストの動作確認は成功しています。しかしフロントのReactで作ったformからloginリクエストなどを飛ばすと419エラーがとなって下記のエラーが返ってきます。

発生している問題・エラーメッセージ

chromeブラウザの検証のnetworkタブで下記を確認

message: "CSRF token mismatch.", exception: "Symfony\Component\HttpKernel\Exception\HttpException"

該当のソースコード

create-react-appで作成

react

1import { useState, useContext } from "react"; 2import axios from "axios" 3import { AuthContext } from "../../context/AuthContext"; 4 5const LoginUser = () => { 6 //inputのstate 7 const [inputEmailVal, setInputEmailVal] = useState(''); 8 9 const [inputPasswordVal, setInputPasswordVal] = useState(''); 10 11 //ログイン状態をtrue,falseで管理(Contextを使用) 12 const { auth, setAuth } = useContext(AuthContext); 13  14 //inputタグのhandler 15 const chengeInput = (e) => { 16 if (e.target.name === 'email') { 17 setInputEmailVal(e.target.value); 18 } else if (e.target.name === 'password') { 19 setInputPasswordVal(e.target.value); 20 } 21 } 22 23 //ログイン処理 24 const login = async (e) => { 25 e.preventDefault(); 26 27 const data = { 28 email: inputEmailVal, 29 password: inputPasswordVal 30 } 31 32 try { 33 //csrfトークンの初期化 34 await axios.get('http://127.0.0.1:8000/sanctum/csrf-cookie', { withCredentials: true }); 35 36 //ログインリクエスト 37 await axios.post('http://127.0.0.1:8000/admins/login', data, { withCredentials: true }) 38 .then(res => { 39 setAuth(true); 40 console.log('ログインしました'); 41 }) 42 43 //ログインしたユーザーの情報取得・表示 44 const result = await axios.get('http://127.0.0.1:8000/api/admin') 45 console.log(result.data); 46 } catch (e) { 47 console.log('ログインできませんでした'); 48 } 49 } 50 51 //ログアウト処理 52 const logout = async (e) => { 53 e.preventDefault(); 54 55 try { 56 //csrfトークンの初期化 57 await axios.get('http://127.0.0.1:8000/sanctum/csrf-cookie'); 58 59 //ログアウトリクエスト 60 const result = await axios.post('http://127.0.0.1:8000/admins/logout'); 61 setAuth(false); 62 console.log('ログアウトしました'); 63 } catch (e) { 64 console.log('ログアウトリクエストに問題がありました') 65 } 66 } 67 68 return ( 69 <> 70 <h3>login画面</h3> 71 //ログイン前の表示内容 72 { 73 !auth && ( 74 <> 75 <form action="" onSubmit={login}> 76 <div> 77 <label htmlFor="email">メースアドレス</label> 78 <input type="email" id="email" onChange={chengeInput} name="email" /> 79 </div> 80 81 <div> 82 <label htmlFor="password">パスワード</label> 83 <input type="text" id="password" onChange={chengeInput} name="password" /> 84 </div> 85 86 <button>ログイン</button> 87 </form> 88 </> 89 ) 90 } 91 92 //ログイン後の表示内容 93 { 94 auth && ( 95 <> 96 <h4>ログイン中</h4> 97 <form action="" onSubmit={logout}> 98 <button>ログアウト</button> 99 </form> 100 </> 101 ) 102 } 103 </> 104 ); 105} 106 107export default LoginUser;

バックエンド(laravel)
routing関連ファイル

laravel

1//routes/web.php 2<?php 3 4use Illuminate\Support\Facades\Route; 5 6 7Route::get('/', function () { 8 return ['Laravel' => app()->version()]; 9}); 10 11require __DIR__.'/auth.php';
//routes/auth.php <?php use App\Http\Controllers\User\Auth\AuthenticatedSessionController; use App\Http\Controllers\User\Auth\EmailVerificationNotificationController; use App\Http\Controllers\User\Auth\NewPasswordController; use App\Http\Controllers\User\Auth\PasswordResetLinkController; use App\Http\Controllers\User\Auth\RegisteredUserController; use App\Http\Controllers\User\Auth\VerifyEmailController; use Illuminate\Support\Facades\Route; Route::post('/register', [RegisteredUserController::class, 'store']) ->middleware('guest') ->name('register'); Route::post('/login', [AuthenticatedSessionController::class, 'store']) ->middleware('guest') ->name('login'); 〜〜〜 Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']) ->middleware('auth:users') ->name('logout');

laravel

1//routes/admin.php 2<?php 3 4use App\Http\Controllers\Admin\Auth\AuthenticatedSessionController; 5〜〜〜 6 7Route::post('/register', [RegisteredUserController::class, 'store']) 8 ->middleware('guest') 9 ->name('register'); 10 11Route::post('/login', [AuthenticatedSessionController::class, 'store']) 12 ->middleware('guest') 13 ->name('login'); 14〜〜〜 15 16Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']) 17 ->middleware('auth:admins') 18 ->name('logout');

config関連コード

laravel

1//config/cors.php 2<?php 3 4return [ 5 'paths' => ['api/*', 'users/*', 'admins/*', 'sanctum/csrf-cookie'], 6 'allowed_methods' => ['*'], 7 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], 8 'allowed_origins_patterns' => [], 9 'allowed_headers' => ['*'], 10 'exposed_headers' => [], 11 'max_age' => 0, 12 'supports_credentials' => true, 13];

laravel

1//config/sanctum.php 2<?php 3 4return [ 5 6 'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 7 '%s%s%s', 8 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 9 env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '', 10 env('FRONTEND_URL') ? ','.parse_url(env('FRONTEND_URL'), PHP_URL_HOST) : '' 11 ))), 12 13 'expiration' => null, 14 15 'middleware' => [ 16 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, 17 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 18 ], 19 20]; 21

laravel

1config/session.php 2<?php 3use Illuminate\Support\Str; 4return [ 5 'driver' => env('SESSION_DRIVER', 'file'), 6 'lifetime' => env('SESSION_LIFETIME', 120), 7 'expire_on_close' => false, 8 'encrypt' => false, 9 'files' => storage_path('framework/sessions'), 10 'connection' => env('SESSION_CONNECTION'), 11 'table' => 'sessions', 12 'store' => env('SESSION_STORE'), 13 'lottery' => [2, 100], 14 'cookie' => env( 15 'SESSION_COOKIE', 16 Str::slug(env('APP_NAME', 'laravel'), '_').'_session' 17 ), 18 'path' => '/', 19 'domain' => env('SESSION_DOMAIN'), 20 'secure' => env('SESSION_SECURE_COOKIE'), 21 'http_only' => true, 22 'same_site' => 'none', 23 24]; 25

middleware関連のコード

laravel

1app/Http/Middleware/VerifyCsrfToken.php 2<?php 3 4namespace App\Http\Middleware; 5 6use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware; 7 8class VerifyCsrfToken extends Middleware 9{ 10 //exceptで/*を指定してcsrf対策を無くすとリクエストが動作する。 11 protected $except = [ 12 // '/*' 13 ]; 14}

画像
イメージ説明

エラーリクエスト部分のヘッダー情報 General Request URL: http://127.0.0.1:8000/admins/login Request Method: POST Status Code: 419 unknown status Remote Address: 127.0.0.1:8000 Referrer Policy: strict-origin-when-cross-origin Response Headers HTTP/1.1 419 unknown status Host: 127.0.0.1:8000 Date: Tue, 22 Aug 2023 16:53:53 GMT Connection: close X-Powered-By: PHP/8.2.0 Cache-Control: no-cache, private Date: Tue, 22 Aug 2023 16:53:53 GMT Content-Type: application/json Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Credentials: true Set-Cookie: laravel_session=ey〜〜〜; expires=Tue, 22 Aug 2023 18:53:53 GMT; Max-Age=7200; path=/; domain=localhost; httponly; samesite=none Request headers POST /admins/login HTTP/1.1 Accept: application/json, text/plain, */* Accept-Encoding: gzip, deflate, br Accept-Language: ja,en-US;q=0.9,en;q=0.8 Connection: keep-alive Content-Length: 55 Content-Type: application/json Host: 127.0.0.1:8000 Origin: http://localhost:3000 Referer: http://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: cross-site 〜〜〜

csrfトークン初期化リクエストのheader情報

General Request URL: http://127.0.0.1:8000/sanctum/csrf-cookie Request Method: GET Status Code: 204 No Content Remote Address: 127.0.0.1:8000 Referrer Policy: strict-origin-when-cross-origin Response Headers HTTP/1.1 204 No Content Host: 127.0.0.1:8000 Date: Tue, 22 Aug 2023 16:53:52 GMT Connection: close X-Powered-By: PHP/8.2.0 Cache-Control: no-cache, private Date: Tue, 22 Aug 2023 16:53:52 GMT Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Credentials: true Set-Cookie: XSRF-TOKEN=ey〜〜〜; expires=Tue, 22 Aug 2023 18:53:52 GMT; Max-Age=7200; path=/; domain=localhost; samesite=none Set-Cookie: laravel_session=ey〜〜〜; expires=Tue, 22 Aug 2023 18:53:52 GMT; Max-Age=7200; path=/; domain=localhost; httponly; samesite=none Request headers GET /sanctum/csrf-cookie HTTP/1.1 Accept: application/json, text/plain, */* Accept-Encoding: gzip, deflate, br Accept-Language: ja,en-US;q=0.9,en;q=0.8 Connection: keep-alive Host: 127.0.0.1:8000 Origin: http://localhost:3000 Referer: http://localhost:3000/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: cross-site 〜〜〜

試したこと

・postmanによるlogin,logout処理はX-XSRF-TOKENをリクエストヘッダーに付与してcsrf対策のされたlogin,logoutリクエストが成功。
・laravel側でmiddlewareのVerifyCsrfToken.phpでcsrf対策をしない設定でフロントからlogin,logoutリクエストを送ると成功。
・axiosによってcookieに保存されているXSRF-TOKENの値がリクエストheaderのX-XSRF-TOKENに設定されてcsrf対策の取れたリクエストとなるはずなのに設定されず419エラーとなる。

自分で考えついた原因
csrf対策で何かおかしいところがある。
csrfトークンの初期化リクエストでSet-Cookieでトークンがちゃんと渡ってきていることを確認したけど、ブラウザのcookieに保存されていない。なのでaxiosが自動でheaderのX-XSRF-TOKENを設定できない。それによってlaravelのcsrf対策に引っ掛かって419エラーとなる。

やったこと:
・axiosでリクエストする際にwithCredentials:trueを設定
・chromeのcookieの設定で「Cookie をすべて受け入れる」に設定

結果:
withCredentials:trueに設定しているがSet-CookieでブラウザにXSRF-TOKENが保存されない。

わかる方いましたらよろしくお願いします。

補足情報(FW/ツールのバージョンなど)

laravel 9
"axios": "^1.4.0"
"react": "^18.2.0"
node 16.14.0
os: macOS Monterey 12.5
MAMP

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

自己解決

自己解決できました。
原因ですがクロスオリジンでのブラウザのcookieの扱いをしっかり理解できていなかったようで、クロスオリジンではcookieのsame-site属性をnoneにしなければクロスオリジンでのpost通信でSet-Cookieがが無視されてしまいブラウザに保存されず、加えてsame-siteをnoneにした場合はcookieのsecure属性も付けなくてはいけなくなり、secure属性がつくとhttpではなくhttpsのリクエストを要求されるので、それがが原因でhttps通信を設定していないMAMPの環境ではSet-CookieでブラウザにXSRF-TOKENが保存されずにエラーとなっていました。

やったこと
・MAMP環境下でのhttps化
参考:https://parashuto.com/rriver/tools/mkcert-for-local-ssl-dev-env
・axiosを使うときにaxiosインスタンスを生成し設定に下記を追加
xsrfHeaderName: 'X-XSRF-TOKEN',
xsrfCookieName: 'XSRF-TOKEN',
withCredentials: true,

これでうまくいきました。
同じようなエラーに遭遇した方の参考になれば幸いです。

投稿2023/08/23 09:49

ryuichi-works

総合スコア40

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.30%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問