fix(csrf-token): add leading dot to session domain cookie. (#224)

* fix(csrf-token): add leading dot to session domain cookie.

* refactor: remove generate key, upgrade axios and keep session domain in null.

* refactor: fix PSR-12 code styles for PHP 8.2 compatibility.

---------

Co-authored-by: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com>
This commit is contained in:
Loduis Madariaga Barrios
2025-08-28 02:44:34 -05:00
committed by GitHub
parent bf0d98c69c
commit 8e96d3e972
13 changed files with 59 additions and 69 deletions

View File

@@ -4,7 +4,7 @@ APP_DEBUG=true
APP_NAME="InvoiceShelf"
APP_LOG_LEVEL=debug
APP_TIMEZONE=UTC
APP_URL=http://invoiceshelf.test
APP_URL=
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
@@ -49,7 +49,6 @@ PUSHER_APP_ID=
PUSHER_KEY=
PUSHER_SECRET=
SANCTUM_STATEFUL_DOMAINS=invoiceshelf.test
TRUSTED_PROXIES="*"
CRON_JOB_AUTH_TOKEN=""

View File

@@ -29,7 +29,10 @@ class DatabaseConfigurationController extends Controller
$results = $this->environmentManager->saveDatabaseVariables($request);
if (array_key_exists('success', $results)) {
Artisan::call('key:generate --force');
// Automatically regenerating the key is disabled to prevent complications in the wizard process.
// This can cause issues with the CSRF token, resulting in "Token Mismatch" or "Invalid CSRF Token" errors.
// It is recommended that the user manually generates the key before running the wizard to ensure application security and stability.
// Artisan::call('key:generate --force');
Artisan::call('optimize:clear');
Artisan::call('config:clear');
Artisan::call('cache:clear');

View File

@@ -104,14 +104,22 @@ class EnvironmentManager
*/
public function saveDatabaseVariables(DatabaseEnvironmentRequest $request)
{
$appUrl = $request->get('app_url');
if ($appUrl !== config('app.url')) {
config(['app.url' => $appUrl]);
}
[$sanctumDomain, $sessionDomain] = $this->getDomains(
$request->getHttpHost()
);
$dbEnv = [
'APP_URL' => $request->get('app_url'),
'APP_URL' => $appUrl,
'APP_LOCALE' => $request->get('app_locale'),
'DB_CONNECTION' => $request->get('database_connection'),
'SANCTUM_STATEFUL_DOMAINS' => $request->get('app_domain'),
'SESSION_DOMAIN' => explode(':', $request->get('app_domain'))[0],
'SESSION_DOMAIN' => $sessionDomain,
];
if ($sanctumDomain !== null) {
$dbEnv['SANCTUM_STATEFUL_DOMAINS'] = $sanctumDomain;
}
if ($dbEnv['DB_CONNECTION'] != 'sqlite') {
if ($request->has('database_username') && $request->has('database_password')) {
$dbEnv['DB_HOST'] = $request->get('database_hostname');
@@ -462,10 +470,16 @@ class EnvironmentManager
public function saveDomainVariables(DomainEnvironmentRequest $request)
{
try {
$this->updateEnv([
'SANCTUM_STATEFUL_DOMAINS' => $request->get('app_domain'),
'SESSION_DOMAIN' => explode(':', $request->get('app_domain'))[0],
]);
[$sanctumDomain, $sessionDomain] = $this->getDomains(
$request->get('app_domain')
);
$domainEnv = [
'SESSION_DOMAIN' => $sessionDomain,
];
if ($sanctumDomain !== null) {
$domainEnv['SANCTUM_STATEFUL_DOMAINS'] = $sanctumDomain;
}
$this->updateEnv($domainEnv);
} catch (Exception $e) {
return [
'error' => 'domain_verification_failed',
@@ -505,4 +519,25 @@ class EnvironmentManager
file_put_contents($this->envPath, trim($formatted));
}
private function getDomains(string $requestDomain): array
{
$appUrl = config('app.url');
$port = parse_url($appUrl, PHP_URL_PORT);
$currentDomain = parse_url($appUrl, PHP_URL_HOST).(
$port ? ':'.$port : ''
);
$requestHost = parse_url($requestDomain, PHP_URL_HOST) ?: $requestDomain;
$isSame = $currentDomain === $requestDomain;
return [
$isSame && env('SANCTUM_STATEFUL_DOMAINS', false) === false ?
null : $requestDomain,
$isSame && env('SESSION_DOMAIN', false) === null ?
null : $requestHost,
];
}
}

2
composer.lock generated
View File

@@ -12363,4 +12363,4 @@
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
}

View File

@@ -1,48 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1,127.0.0.1:8000,::1')),
/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. If this value is null, personal access tokens do
| not expire. This won't tweak the lifetime of first-party sessions.
|
*/
'expiration' => null,
/*
|--------------------------------------------------------------------------
| Sanctum Middleware
|--------------------------------------------------------------------------
|
| When authenticating your first-party SPA with Sanctum you may need to
| customize some of the middleware Sanctum uses while processing the
| request. You may change the middleware listed below as required.
|
*/
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
],
];

View File

@@ -60,4 +60,4 @@
"vue-router": "^4.5.0",
"vuedraggable": "^4.1.0"
}
}
}

View File

@@ -185,7 +185,7 @@ export const useInstallationStore = (useWindow = false) => {
})
},
checkAutheticated() {
checkAuthenticated() {
return new Promise((resolve, reject) => {
axios
.get(`/api/v1/auth/check`)

View File

@@ -87,7 +87,7 @@ async function verifyDomain() {
try {
await installationStore.setInstallationDomain(formData)
await installationStore.installationLogin()
let driverRes = await installationStore.checkAutheticated()
let driverRes = await installationStore.checkAuthenticated()
if (driverRes.data) {
emit('next', 4)

View File

@@ -1,6 +1,6 @@
<template>
<form action="" @submit.prevent="next">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:mb-6 md:mb-6">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 mb-6">
<BaseInputGroup
:label="$t('wizard.database.app_url')"
:error="v$.app_url.$error && v$.app_url.$errors[0].$message"

View File

@@ -1,6 +1,6 @@
<template>
<form action="" @submit.prevent="next">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:mb-6 md:mb-6">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 mb-6">
<BaseInputGroup
:label="$t('wizard.database.app_url')"
:content-loading="isFetchingInitialData"

View File

@@ -1,6 +1,6 @@
<template>
<form action="" @submit.prevent="next">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:mb-6 md:mb-6">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 mb-6">
<BaseInputGroup
:label="$t('wizard.database.app_url')"
:content-loading="isFetchingInitialData"

View File

@@ -20,11 +20,11 @@ axios.interceptors.request.use(function (config) {
const authToken = Ls.get('auth.token')
if (authToken) {
config.headers.common.Authorization = authToken
config.headers.Authorization = authToken
}
if (companyId) {
config.headers.common['company'] = companyId
config.headers.company = companyId
}
return config

View File

@@ -113,7 +113,8 @@ Route::prefix('/customer')->group(function () {
Route::get('/installation', function () {
return view('app');
})->name('install')->middleware('redirect-if-installed');
})->name('install')
->middleware(['redirect-if-installed']);
// Move other http requests to the Vue App
// -------------------------------------------------