Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,25 @@ DB_DATABASE=secpal
DB_USERNAME=secpal
DB_PASSWORD=

SESSION_DRIVER=database
SESSION_DRIVER=cookie
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
SESSION_SECURE_COOKIE=false
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax

# Sanctum SPA Configuration
SANCTUM_STATEFUL_DOMAINS=localhost:5173,localhost:3000,127.0.0.1:5173

# CORS Configuration for SPA
# Allow credentials (cookies) from frontend
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173
CORS_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=Content-Type,Authorization,X-Requested-With,X-XSRF-TOKEN
CORS_SUPPORTS_CREDENTIALS=true
CORS_MAX_AGE=86400

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
Expand Down
5 changes: 5 additions & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
$middleware->api(append: [
\App\Http\Middleware\SetLocaleFromHeader::class,
]);

// Configure CORS for SPA authentication with credentials
$middleware->validateCsrfTokens(except: [
// CSRF protection is active - Sanctum middleware handles validation for authenticated SPA routes
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
Expand Down
42 changes: 42 additions & 0 deletions config/cors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

// SPDX-FileCopyrightText: 2025 SecPal Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

return [

/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| For SPA authentication with httpOnly cookies, CORS must:
| - Allow credentials (supports_credentials = true)
| - Specify allowed origins explicitly (cannot use '*')
| - Include CSRF token header (X-XSRF-TOKEN)
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/

'paths' => ['api/*', 'v1/*', 'sanctum/csrf-cookie'],

'allowed_methods' => explode(',', (string) env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS')),

'allowed_origins' => explode(',', (string) env('CORS_ALLOWED_ORIGINS', 'http://localhost:5173,http://localhost:3000')),

'allowed_origins_patterns' => [],

'allowed_headers' => explode(',', (string) env('CORS_ALLOWED_HEADERS', 'Content-Type,Authorization,X-Requested-With,X-XSRF-TOKEN')),

'exposed_headers' => [],

'max_age' => (int) env('CORS_MAX_AGE', 86400),

'supports_credentials' => (bool) env('CORS_SUPPORTS_CREDENTIALS', true),

];
86 changes: 86 additions & 0 deletions config/sanctum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

// SPDX-FileCopyrightText: 2025 SecPal Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

use Laravel\Sanctum\Sanctum;

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(',', (string) env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s,%s',
'localhost:5173,localhost:3000,127.0.0.1:5173',
Sanctum::currentApplicationUrlWithPort()
))),

/*
|--------------------------------------------------------------------------
| Sanctum Guards
|--------------------------------------------------------------------------
|
| This array contains the authentication guards that will be checked when
| Sanctum is trying to authenticate a request. If none of these guards
| are able to authenticate the request, Sanctum will use the bearer
| token that's present on an incoming request for authentication.
|
*/

'guard' => ['web'],

/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. This will override any values set in the token's
| "expires_at" attribute, but first-party sessions are not affected.
|
*/

'expiration' => null,

/*
|--------------------------------------------------------------------------
| Token Prefix
|--------------------------------------------------------------------------
|
| Sanctum can prefix new tokens in order to take advantage of numerous
| security scanning initiatives maintained by open source platforms
| that notify developers if they commit tokens into repositories.
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
*/

'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),

/*
|--------------------------------------------------------------------------
| 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,
],

];
67 changes: 67 additions & 0 deletions tests/Feature/Auth/SanctumSpaConfigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

// SPDX-FileCopyrightText: 2025 SecPal Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

describe('Sanctum SPA Authentication Configuration', function () {
test('sanctum stateful domains configuration is set', function () {
$statefulDomains = config('sanctum.stateful');

expect($statefulDomains)->toBeArray()
->and($statefulDomains)->not->toBeEmpty();
});

test('sanctum middleware includes csrf validation', function () {
$middleware = config('sanctum.middleware');

expect($middleware)->toHaveKey('validate_csrf_token')
->and($middleware['validate_csrf_token'])->toBe(
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class
);
});

test('sanctum guard uses web guard', function () {
$guard = config('sanctum.guard');

expect($guard)->toBe(['web']);
});

test('csrf cookie route is accessible', function () {
$response = $this->get('/sanctum/csrf-cookie');

$response->assertNoContent();
});

test('session driver is configured for cookie storage', function () {
$driver = config('session.driver');

// In testing environment, driver is 'array' for performance
// In production/development, it should be 'cookie' for SPA auth
expect($driver)->toBeIn(['cookie', 'array']);
});

test('session cookies are configured as httpOnly', function () {
$httpOnly = config('session.http_only');

expect($httpOnly)->toBeTrue();
});

test('session uses sameSite lax for CSRF protection', function () {
$sameSite = config('session.same_site');

expect($sameSite)->toBe('lax');
});
});

describe('CORS Configuration for SPA', function () {
test('sanctum stateful domains include frontend development server', function () {
$statefulDomains = config('sanctum.stateful');

expect($statefulDomains)->toContain('localhost:5173')
->and($statefulDomains)->toContain('localhost:3000');
});
});