実現したいこと
ここに実現したいことを箇条書きで書いてください。
- フロントを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

回答1件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。