I’m new to Laravel & react
I have a web api using Laravel 11 for the backend and react frontend
the authentication works OK with Postman for
http://localhost:8000/api/v1/login 200OK
http://127.0.0.1:8000/sanctum/csrf-cookie
returns
{
“ok”: true
}
http://127.0.0.1:8000/api/v1/logout as well
All works ok if we use Postman
but if we use react then we have an issue with the LOGOUT only
there is no issue with the Login the issue is only with the Logout
here is my code and the error.
Api.js
import axios from 'axios';
// API URL from the environment for versioned endpoints (e.g., /api/v1)
const API_URL = import.meta.env.VITE_REACT_APP_API_URL;
// CSRF URL from the environment for non-versioned requests
const CSRF_URL = import.meta.env.VITE_REACT_APP_CSRF_URL;
// Create axios instance for versioned API requests (/api/v1)
const api = axios.create({
baseURL: import.meta.env.VITE_REACT_APP_API_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
// Create axios instance for CSRF token requests (without /api/v1 prefix)
const csrfApi = axios.create({
baseURL: CSRF_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true, // Ensure cookies are sent with CSRF requests
});
// Log all axios requests and responses for debugging purposes
api.interceptors.request.use(
(config) => {
console.log("Step 1: Sending request to API", config.url);
console.log("Step 2: Request Headers: ", config.headers);
console.log("Step 3: Cookies (withCredentials enabled):", document.cookie); // Log cookies to ensure they're being sent
return config;
},
(error) => {
console.error("Step 4: Request error", error);
return Promise.reject(error);
}
);
api.interceptors.response.use(
(response) => {
console.log("Step 5: API Response Success", response);
return response;
},
(error) => {
console.error("Step 6: API Response Error", error.response ? error.response.data : error);
return Promise.reject(error);
}
);
// Function to get CSRF token from Sanctum
export const getCsrfToken = () => {
console.log("Step 7: Fetching CSRF token...");
return csrfApi.get('/sanctum/csrf-cookie')
.then((response) => {
console.log("Step 8: CSRF token fetched successfully", response.data);
console.log("Step 9: Current Cookies after CSRF fetch", document.cookie); // Ensure the CSRF cookie is present
})
.catch((error) => {
console.error("Step 10: CSRF fetch error", error.response ? error.response.data : error);
});
};
// Handle the login request
// Handle the login request
export const login = (credentials) => {
console.log("Logging in with credentials", credentials);
return getCsrfToken().then(() =>
api.post('/login', credentials, { withCredentials: true }) // No need to manually handle the token
.then((response) => {
console.log("Login successful, response:", response.data);
return response.data;
})
.catch((error) => {
console.error("Login failed", error.response ? error.response.data : error);
return Promise.reject(error);
})
);
};
export const logout = () => {
return getCsrfToken().then(() =>
api.post(import.meta.env.VITE_REACT_APP_LOGOUT_ENDPOINT, {}, {
withCredentials: true, // This will ensure the HttpOnly cookie is sent with the request
})
.then((response) => {
console.log("Step 15: Logout successful:", response.data);
return response.data;
})
.catch((error) => {
console.error("Step 16: Logout failed", error.response ? error.response.data : error);
return Promise.reject(error);
})
);
};
// Get the authenticated user
export const getUser = () => {
console.log("Step 17: Fetching authenticated user data...");
return api.get('/user')
.then((response) => {
console.log("Step 18: User data fetched:", response.data);
return response.data;
})
.catch((error) => {
console.error("Step 19: Fetch user data failed", error.response ? error.response.data : error);
return Promise.reject(error);
});
};
export default api;
AppHeaderDropdown.js
import React, { useState } from 'react';
import {
CAvatar,
CBadge,
CDropdown,
CDropdownDivider,
CDropdownHeader,
CDropdownItem,
CDropdownMenu,
CDropdownToggle,
CSpinner,
} from '@coreui/react-pro';
import { useNavigate } from 'react-router-dom';
import CIcon from '@coreui/icons-react';
import {
cilBell,
cilCreditCard,
cilCommentSquare,
cilEnvelopeOpen,
cilFile,
cilLockLocked,
cilSettings,
cilTask,
cilUser,
cilAccountLogout,
} from '@coreui/icons';
import avatar8 from './../../assets/images/avatars/8.jpg';
import { logout } from '../../services/api';
import { useTranslation } from 'react-i18next';
const AppHeaderDropdown = () => {
const navigate = useNavigate();
const [isLoggingOut, setIsLoggingOut] = useState(false);
const { t } = useTranslation();
const handleLogout = async () => {
setIsLoggingOut(true);
console.log("Step 20: Logout button clicked");
try {
const result = await logout();
console.log("Step 21: Logout result:", result);
navigate('/login', { replace: true });
} catch (error) {
console.error("Step 22: Logout failed:", error);
}
setIsLoggingOut(false);
};
if (isLoggingOut) {
return (
<div className="d-flex justify-content-center align-items-center min-vh-100">
<CSpinner color="primary" variant="grow" /> {t('logging_out')}
</div>
);
}
return (
<CDropdown variant="nav-item" alignment="end">
<CDropdownToggle className="py-0" caret={false}>
<CAvatar src={avatar8} size="md" />
<CBadge color="warning-gradient" className="ms-2">42</CBadge>
</CDropdownItem>
<CDropdownHeader className="bg-body-secondary text-body-secondary fw-semibold my-2">
{t('settings')}
</CDropdownHeader>
<CDropdownItem onClick={handleLogout}>
<CIcon icon={cilAccountLogout} className="me-2" />
{t('logout')}
</CDropdownItem>
</CDropdownMenu>
</CDropdown>
);
};
export default AppHeaderDropdown;
sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort()
))),
'guard' => ['web'],
'expiration' => 60,
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
cors.php
<?php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:3000'], // Replace with your frontend URL
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true, // This ensures that cookies are allowed
];
AuthController.php
<?php
namespace AppHttpControllers;
use AppModelsUser;
use AppModelsUserProvider;
use IlluminateAuthEventsPasswordReset;
use IlluminateAuthEventsRegistered;
use IlluminateAuthEventsVerified;
use IlluminateHttpJsonResponse;
use IlluminateHttpRedirectResponse;
use IlluminateHttpRequest;
use IlluminateSupportFacadesCrypt;
use IlluminateSupportFacadesHash;
use IlluminateSupportFacadesPassword;
use IlluminateSupportStr;
use IlluminateValidationRules;
use IlluminateValidationValidationException;
use IlluminateViewView;
use LaravelSocialiteFacadesSocialite;
class AuthController extends Controller
{
/**
* Register new user
*/
public function register(Request $request): JsonResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
'password' => ['required', 'confirmed', RulesPassword::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$user->ulid = Str::ulid()->toBase32();
$user->save();
$user->assignRole('user');
event(new Registered($user));
return response()->json([
'ok' => true,
], 201);
}
/**
* Redirect to provider for authentication
*/
public function redirect(Request $request, string $provider): RedirectResponse
{
return Socialite::driver($provider)->stateless()->redirect();
}
/**
* Handle callback from provider
* @throws Exception
*/
public function callback(Request $request, string $provider): View
{
$oAuthUser = Socialite::driver($provider)->stateless()->user();
if (!$oAuthUser?->token) {
return view('oauth', [
'message' => [
'ok' => false,
'message' => __('Unable to authenticate with :provider', ['provider' => $provider]),
],
]);
}
$userProvider = UserProvider::select('id', 'user_id')
->where('name', $provider)
->where('provider_id', $oAuthUser->id)
->first();
if (!$userProvider) {
if (User::where('email', $oAuthUser->email)->exists()) {
return view('oauth', [
'message' => [
'ok' => false,
'message' => __('Unable to authenticate with :provider. User with email :email already exists. To connect a new service to your account, you can go to your account settings and go through the process of linking your account.', [
'provider' => $provider,
'email' => $oAuthUser->email,
]),
],
]);
}
$user = new User();
$user->ulid = Str::ulid()->toBase32();
$user->avatar = $oAuthUser->picture ?? $oAuthUser->avatar_original ?? $oAuthUser->avatar;
$user->name = $oAuthUser->name;
$user->email = $oAuthUser->email;
$user->password = null;
$user->email_verified_at = now();
$user->save();
$user->assignRole('user');
$user->userProviders()->create([
'provider_id' => $oAuthUser->id,
'name' => $provider,
]);
} else {
$user = $userProvider->user;
}
$token = $user->createDeviceToken(
device: $request->deviceName(),
ip: $request->ip(),
remember: true
);
return view('oauth', [
'message' => [
'ok' => true,
'provider' => $provider,
'token' => $token,
],
]);
}
/**
* Generate sanctum token on successful login
* @throws ValidationException
*/
public function login(Request $request): JsonResponse
{
$request->validate([
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
// Create the token
$token = $user->createToken('auth_token')->plainTextToken;
// Set the token as HttpOnly cookie
return response()->json([
'ok' => true,
])->cookie(
'token', // cookie name
$token, // token value
60 * 24 * 7, // cookie expiration time in minutes (1 week)
'/', // path
null, // domain (null will set for the current domain)
true, // secure (only HTTPS)
true // HttpOnly (not accessible via JavaScript)
);
}
/**
* Revoke token; only remove token that is used to perform logout (i.e. will not revoke all tokens)
*/
public function logout(Request $request)
{
// Revoke the current user's token (Sanctum)
try {
$request->user()->currentAccessToken()->delete(); // Delete the current token
return response()->json(['ok' => true, 'message' => 'Logged out successfully.']);
} catch (Exception $e) {
return response()->json(['ok' => false, 'message' => 'Logout failed.'], 500);
}
}
/**
* Get authenticated user details
*/
public function user(Request $request): JsonResponse
{
$user = $request->user();
return response()->json([
'ok' => true,
'user' => [
...$user->toArray(),
'must_verify_email' => $user->mustVerifyEmail(),
'has_password' => (bool) $user->password,
'roles' => $user->roles()->select('name')->pluck('name'),
'providers' => $user->userProviders()->select('name')->pluck('name'),
],
]);
}
/**
* Handle an incoming password reset link request.
* @throws ValidationException
*/
public function sendResetPasswordLink(Request $request): JsonResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
if ($status !== Password::RESET_LINK_SENT) {
throw ValidationException::withMessages([
'email' => [__($status)],
]);
}
return response()->json([
'ok' => true,
'message' => __($status),
]);
}
/**
* Handle an incoming new password request.
* @throws ValidationException
*/
public function resetPassword(Request $request): JsonResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email', 'exists:' . User::class],
'password' => ['required', 'confirmed', RulesPassword::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
static function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
if ($status !== Password::PASSWORD_RESET) {
throw ValidationException::withMessages([
'email' => [__($status)],
]);
}
return response()->json([
'ok' => true,
'message' => __($status),
]);
}
/**
* Mark the authenticated user's email address as verified.
*/
public function verifyEmail(Request $request, string $ulid, string $hash): JsonResponse
{
$user = User::where('ulid', $ulid)->first();
abort_if(!$user, 404);
abort_if(!hash_equals(sha1($user->getEmailForVerification()), $hash), 403, __('Invalid verification link'));
if (!$user->hasVerifiedEmail()) {
$user->markEmailAsVerified();
event(new Verified($user));
}
return response()->json([
'ok' => true,
]);
}
/**
* Send a new email verification notification.
*/
public function verificationNotification(Request $request): JsonResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
$user = $request->user() ?: User::where('email', $request->email)->whereNull('email_verified_at')->first();
abort_if(!$user, 400);
$user->sendEmailVerificationNotification();
return response()->json([
'ok' => true,
'message' => __('Verification link sent!'),
]);
}
/**
* Get authenticated user devices
*/
public function devices(Request $request): JsonResponse
{
$user = $request->user();
$devices = $user->tokens()
->select('id', 'name', 'ip', 'last_used_at')
->orderBy('last_used_at', 'DESC')
->get();
$currentToken = $user->currentAccessToken();
foreach ($devices as $device) {
$device->hash = Crypt::encryptString($device->id);
if ($currentToken->id === $device->id) {
$device->is_current = true;
}
unset($device->id);
}
return response()->json([
'ok' => true,
'devices' => $devices,
]);
}
/**
* Revoke token by id
*/
public function deviceDisconnect(Request $request): JsonResponse
{
$request->validate([
'hash' => 'required',
]);
$user = $request->user();
$id = (int) Crypt::decryptString($request->hash);
if (!empty($id)) {
$user->tokens()->where('id', $id)->delete();
}
return response()->json([
'ok' => true,
]);
}
}
and here is the output
these steps for login
Download the React DevTools for a better development experience: https://reactjs.org/link/react-devtools
api.js:68 Step 11: Logging in with credentials {email: '[email protected]', password: 'password123'}
api.js:54 Step 7: Fetching CSRF token...
api.js:57 Step 8: CSRF token fetched successfully {ok: true}
api.js:58 Step 9: Current Cookies after CSRF fetch
api.js:30 Step 1: Sending request to API /login
api.js:31 Step 2: Request Headers: AxiosHeaders {Accept: 'application/json, text/plain, */*', Content-Type: 'application/json'}
api.js:32 Step 3: Cookies (withCredentials enabled):
csrf-cookie:1
Third-party cookie will be blocked in future Chrome versions as part of Privacy Sandbox.Understand this warning
2127.0.0.1:8000/api/v1/login:1
Third-party cookie will be blocked in future Chrome versions as part of Privacy Sandbox.Understand this warning
api.js:43 Step 5: API Response Success {data: {…}, status: 200, statusText: 'OK', headers: AxiosHeaders, config: {…}, …}
api.js:72 Step 12: Login successful, token received: {ok: true}
Login.js:41 Login successful, token: undefined
===========================
and these steps for logout
Step 20: Logout button clicked
api.js:54 Step 7: Fetching CSRF token...
api.js:57 Step 8: CSRF token fetched successfully {ok: true}
api.js:58 Step 9: Current Cookies after CSRF fetch
api.js:30 Step 1: Sending request to API /logout
api.js:31 Step 2: Request Headers: AxiosHeaders {Accept: 'application/json, text/plain, */*', Content-Type: 'application/json'}
api.js:32 Step 3: Cookies (withCredentials enabled):
api.js:108
POST http://127.0.0.1:8000/api/v1/logout 401 (Unauthorized)
dispatchXhrRequest @ axios.js?v=7d171547:1680
xhr @ axios.js?v=7d171547:1560
dispatchRequest @ axios.js?v=7d171547:2035
Promise.then
_request @ axios.js?v=7d171547:2222
request @ axios.js?v=7d171547:2141
httpMethod @ axios.js?v=7d171547:2269
wrap @ axios.js?v=7d171547:8
(anonymous) @ api.js:108
Promise.then
logout @ api.js:107
handleLogout @ AppHeaderDropdown.js:41
callCallback2 @ chunk-VGGCA2L5.js?v=724d8ce0:3674
invokeGuardedCallbackDev @ chunk-VGGCA2L5.js?v=724d8ce0:3699
invokeGuardedCallback @ chunk-VGGCA2L5.js?v=724d8ce0:3733
invokeGuardedCallbackAndCatchFirstError @ chunk-VGGCA2L5.js?v=724d8ce0:3736
executeDispatch @ chunk-VGGCA2L5.js?v=724d8ce0:7014
processDispatchQueueItemsInOrder @ chunk-VGGCA2L5.js?v=724d8ce0:7034
processDispatchQueue @ chunk-VGGCA2L5.js?v=724d8ce0:7043
dispatchEventsForPlugins @ chunk-VGGCA2L5.js?v=724d8ce0:7051
(anonymous) @ chunk-VGGCA2L5.js?v=724d8ce0:7174
batchedUpdates$1 @ chunk-VGGCA2L5.js?v=724d8ce0:18913
batchedUpdates @ chunk-VGGCA2L5.js?v=724d8ce0:3579
dispatchEventForPluginEventSystem @ chunk-VGGCA2L5.js?v=724d8ce0:7173
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-VGGCA2L5.js?v=724d8ce0:5478
dispatchEvent @ chunk-VGGCA2L5.js?v=724d8ce0:5472
dispatchDiscreteEvent @ chunk-VGGCA2L5.js?v=724d8ce0:5449
Show 22 more frames
Show lessUnderstand this error
api.js:47 Step 6: API Response Error {ok: false, message: 'Unauthenticated.'}
(anonymous) @ api.js:47
Promise.then
_request @ axios.js?v=7d171547:2222
request @ axios.js?v=7d171547:2141
httpMethod @ axios.js?v=7d171547:2269
wrap @ axios.js?v=7d171547:8
(anonymous) @ api.js:108
Promise.then
logout @ api.js:107
handleLogout @ AppHeaderDropdown.js:41
callCallback2 @ chunk-VGGCA2L5.js?v=724d8ce0:3674
invokeGuardedCallbackDev @ chunk-VGGCA2L5.js?v=724d8ce0:3699
invokeGuardedCallback @ chunk-VGGCA2L5.js?v=724d8ce0:3733
invokeGuardedCallbackAndCatchFirstError @ chunk-VGGCA2L5.js?v=724d8ce0:3736
executeDispatch @ chunk-VGGCA2L5.js?v=724d8ce0:7014
processDispatchQueueItemsInOrder @ chunk-VGGCA2L5.js?v=724d8ce0:7034
processDispatchQueue @ chunk-VGGCA2L5.js?v=724d8ce0:7043
dispatchEventsForPlugins @ chunk-VGGCA2L5.js?v=724d8ce0:7051
(anonymous) @ chunk-VGGCA2L5.js?v=724d8ce0:7174
batchedUpdates$1 @ chunk-VGGCA2L5.js?v=724d8ce0:18913
batchedUpdates @ chunk-VGGCA2L5.js?v=724d8ce0:3579
dispatchEventForPluginEventSystem @ chunk-VGGCA2L5.js?v=724d8ce0:7173
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-VGGCA2L5.js?v=724d8ce0:5478
dispatchEvent @ chunk-VGGCA2L5.js?v=724d8ce0:5472
dispatchDiscreteEvent @ chunk-VGGCA2L5.js?v=724d8ce0:5449
Show 19 more frames
Show lessUnderstand this error
api.js:116 Step 16: Logout failed {ok: false, message: 'Unauthenticated.'}
(anonymous) @ api.js:116
Promise.catch
(anonymous) @ api.js:115
Promise.then
logout @ api.js:107
handleLogout @ AppHeaderDropdown.js:41
callCallback2 @ chunk-VGGCA2L5.js?v=724d8ce0:3674
invokeGuardedCallbackDev @ chunk-VGGCA2L5.js?v=724d8ce0:3699
invokeGuardedCallback @ chunk-VGGCA2L5.js?v=724d8ce0:3733
invokeGuardedCallbackAndCatchFirstError @ chunk-VGGCA2L5.js?v=724d8ce0:3736
executeDispatch @ chunk-VGGCA2L5.js?v=724d8ce0:7014
processDispatchQueueItemsInOrder @ chunk-VGGCA2L5.js?v=724d8ce0:7034
processDispatchQueue @ chunk-VGGCA2L5.js?v=724d8ce0:7043
dispatchEventsForPlugins @ chunk-VGGCA2L5.js?v=724d8ce0:7051
(anonymous) @ chunk-VGGCA2L5.js?v=724d8ce0:7174
batchedUpdates$1 @ chunk-VGGCA2L5.js?v=724d8ce0:18913
batchedUpdates @ chunk-VGGCA2L5.js?v=724d8ce0:3579
dispatchEventForPluginEventSystem @ chunk-VGGCA2L5.js?v=724d8ce0:7173
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-VGGCA2L5.js?v=724d8ce0:5478
dispatchEvent @ chunk-VGGCA2L5.js?v=724d8ce0:5472
dispatchDiscreteEvent @ chunk-VGGCA2L5.js?v=724d8ce0:5449
Show 15 more frames
Show lessUnderstand this error
AppHeaderDropdown.js:45 Step 22: Logout failed: AxiosError {message: 'Request failed with status code 401', name: 'AxiosError', code: 'ERR_BAD_REQUEST', config: {…}, request: XMLHttpRequest, …}
handleLogout @ AppHeaderDropdown.js:45
await in handleLogout
callCallback2 @ chunk-VGGCA2L5.js?v=724d8ce0:3674
invokeGuardedCallbackDev @ chunk-VGGCA2L5.js?v=724d8ce0:3699
invokeGuardedCallback @ chunk-VGGCA2L5.js?v=724d8ce0:3733
invokeGuardedCallbackAndCatchFirstError @ chunk-VGGCA2L5.js?v=724d8ce0:3736
executeDispatch @ chunk-VGGCA2L5.js?v=724d8ce0:7014
processDispatchQueueItemsInOrder @ chunk-VGGCA2L5.js?v=724d8ce0:7034
processDispatchQueue @ chunk-VGGCA2L5.js?v=724d8ce0:7043
dispatchEventsForPlugins @ chunk-VGGCA2L5.js?v=724d8ce0:7051
(anonymous) @ chunk-VGGCA2L5.js?v=724d8ce0:7174
batchedUpdates$1 @ chunk-VGGCA2L5.js?v=724d8ce0:18913
batchedUpdates @ chunk-VGGCA2L5.js?v=724d8ce0:3579
dispatchEventForPluginEventSystem @ chunk-VGGCA2L5.js?v=724d8ce0:7173
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-VGGCA2L5.js?v=724d8ce0:5478
dispatchEvent @ chunk-VGGCA2L5.js?v=724d8ce0:5472
dispatchDiscreteEvent @ chunk-VGGCA2L5.js?v=724d8ce0:5449
Show 15 more frames
Show lessUnderstand this error