Session ID Changes Between AJAX POSTs to Plugin API - Token Validation Fails

Hi,

I’m developing a chat plugin and encountering a persistent issue where the session_id (obtained via Session::getId()) changes between two sequential AJAX POST requests from the client to my plugin’s API routes. This causes a consent token, tied to the initial session_id, to become unresolvable.

Problem Overview:

  1. Consent: User gives consent via the chat.
    • An AJAX POST hits [code]/api/myvendor/myplugin/consent[/code].
    • In ChatApiController::handleConsentLog(), a unique X-Consent-Token is generated and stored in the myvendor_myplugin_consent_tokens table alongside Session::getId() (let’s call this session_id_A). The token is sent to the client and stored in localStorage.
  2. Send Message (Proxy): User sends a chat message.
    • An AJAX POST hits [code]/api/myvendor/myplugin/proxy[/code] with the X-Consent-Token and X-CSRF-TOKEN headers.
    • In ChatApiController::handleProxy(), the token is validated by checking for a matching token AND the current Session::getId() (let’s call this session_id_B).
    • Issue: session_id_A and session_id_B are different, so the token stored with session_id_A is not found when querying with session_id_B.

Log Snippet (from handleProxy illustrating the issue):

[2025-06-02 22:21:05] local.DEBUG: [MyPluginAPI] Proxy: Current Server Session ID (start of request): 'EFLpZ...Bc' [this is session_id_B]
[2025-06-02 22:21:05] local.DEBUG: [MyPluginAPI] Proxy: Searching for token in DB: session_id = 'EFLpZ...Bc', token = '4a68...'
[2025-06-02 22:21:05] local.DEBUG: [MyPluginAPI] Proxy: Token NOT FOUND in DB for combination session_id 'EFLpZ...Bc' and token '4a68...'.
[2025-06-02 22:21:05] local.DEBUG: [MyPluginAPI] Proxy (Debug): Tokens in DB with current session_id 'EFLpZ...Bc': []
[2025-06-02 22:21:05] local.DEBUG: [MyPluginAPI] Proxy (Debug): Sessions in DB with received token '4a68...': [{"id":24,"session_id":"mbXhF...Mo", ...}] [this is session_id_A, with which the token was stored]

(Note: Debug logs confirm the session ID is stable within a single request.)

Key Configuration:

  • Effective session settings (from .env and config/session.php):
    • APP_URL: https://example.com
    • SESSION_DRIVER: file
    • SESSION_LIFETIME: 120
    • SESSION_DOMAIN: .example.com
    • SESSION_SECURE_COOKIE: true
    • SESSION_COOKIE_PATH: /
    • SESSION_HTTP_ONLY: true
    • SESSION_SAME_SITE: lax
    • SESSION_COOKIE_NAME: myapp_session (example name)

Relevant Code Snippets:

  • plugins/myvendor/myplugin/routes.php:

        use MyVendor\MyPlugin\Classes\ChatApiController; // Anonymized namespace
        use Illuminate\Support\Facades\Route;
    
        Route::group(['prefix' => 'api/myvendor/myplugin'], function () { // Anonymized path
            Route::post('proxy', fn(Illuminate\Http\Request $request) => (new ChatApiController())->handleProxy($request));
            Route::post('consent', fn(Illuminate\Http\Request $request) => (new ChatApiController())->handleConsentLog($request));
        });
  • JavaScript fetch (in assets/js/script.js):

        // Consent call
        const response = await fetch(CONSENT_LOG_URL, { /* ... headers ... */ credentials: 'include' });
        // Proxy call
        const response = await fetch(PROXY_URL, { /* ... headers ... */ credentials: 'include' });
  • ChatApiController.php (simplified Session::getId() usage):
    In handleConsentLog():

        $sessionId = Session::getId(); // session_id_A
        // ... generate token ...
        Db::table('myvendor_myplugin_consent_tokens')->insert(["session_id" => $sessionId, "token" => $newToken, /* ... */]);

    In handleProxy():

        $sessionId = Session::getId(); // session_id_B
        $dbTokenDetails = Db::table('myvendor_myplugin_consent_tokens')
                            ->where('session_id', $sessionId) // Fails here
                            ->where('token', $clientToken)->first();

Troubleshooting Done:

  1. Session Configuration: Verified settings for domain, secure, http_only, same_site (all seem correct).
  2. JavaScript fetch: Added credentials: 'include' to all relevant fetch calls.
  3. CORS: The application runs entirely under https://example.com, so cross-origin issues seem unlikely.
  4. CSRF Tokens: Handled correctly.
  5. Middleware: Plugin API routes should be using the web middleware group by default (includes StartSession).

My Questions:

  1. Despite the above, what could cause Session::getId() to return different values for sequential AJAX POSTs from the same client to the same October CMS instance?
  2. Are there any specific October CMS or Laravel considerations for session handling in plugin API routes that might be overlooked?
  3. How can I reliably stabilize the session_id for these API interactions?
  4. Any further diagnostic steps or potential fixes you can suggest?

Any insights or suggestions would be greatly appreciated as I want to ensure this chat functionality is secure and reliable.

Turns out it was a stupid mistake on my part. I had forgotten to include the web middleware on my API routes.

Here’s the corrected version:

Route::group([
    'prefix' => 'api/myvendor/myplugin',
    'middleware' => 'web'
], function () {
    Route::post('proxy', fn(Illuminate\Http\Request $request) => (new ChatApiController())->handleProxy($request));
        Route::post('consent', fn(Illuminate\Http\Request $request) => (new ChatApiController())->handleConsentLog($request));
});