Files
timebank-cc-public/references/DOCKER_SESSION_FIX.md
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

9.5 KiB

Docker Login Session Fix

Problem

After updating .env to use Dockerized services (MySQL, Redis), users experienced a login redirect loop. After successful authentication (POST to /login returned 200 OK), the subsequent GET request to /dashboard would redirect back to /login, making it impossible to stay logged in.

Root Cause

Laravel's authentication system calls session->migrate(true) during login to regenerate the session ID for security purposes. This occurs in two places:

  1. SessionGuard::updateSession() - Called by AttemptToAuthenticate action
  2. PrepareAuthenticatedSession - Fortify's login pipeline action

In the Docker environment, the session migration was not persisting correctly, causing the new session ID to not be recognized on subsequent requests, which triggered the authentication middleware to redirect back to login.

Solution Overview

Create a custom DockerSessionGuard that skips session migration during login while maintaining security by regenerating the CSRF token. This guard is only used when IS_DOCKER=true in the environment.

Files Changed

1. Created: app/Auth/DockerSessionGuard.php

Purpose: Custom session guard that overrides updateSession() to skip session->migrate()

<?php

namespace App\Auth;

use Illuminate\Auth\SessionGuard;

class DockerSessionGuard extends SessionGuard
{
    /**
     * Update the session with the given ID.
     *
     * @param  string  $id
     * @return void
     */
    protected function updateSession($id)
    {
        $this->session->put($this->getName(), $id);

        // In Docker, skip session migration to avoid session persistence issues
        // Only regenerate the CSRF token, don't migrate the session ID
        $this->session->regenerateToken();

        // Note: We intentionally skip session->migrate() here for Docker compatibility
        // In production, you should use the standard SessionGuard
    }
}

2. Modified: app/Providers/AuthServiceProvider.php

Purpose: Register the custom guard conditionally when running in Docker

Changes:

  • Added use App\Auth\DockerSessionGuard;
  • Added use Illuminate\Support\Facades\Auth;
  • Added guard registration in boot() method:
public function boot()
{
    $this->registerPolicies();

    // Use custom guard in Docker that doesn't migrate sessions
    if (env('IS_DOCKER', false)) {
        Auth::extend('session', function ($app, $name, array $config) {
            $provider = Auth::createUserProvider($config['provider']);
            $guard = new DockerSessionGuard($name, $provider, $app['session.store']);

            // Set the cookie jar on the guard
            $guard->setCookieJar($app['cookie']);

            // If a request is available, set it on the guard
            if (method_exists($guard, 'setRequest')) {
                $guard->setRequest($app->refresh('request', $guard, 'setRequest'));
            }

            return $guard;
        });
    }

    // ... rest of the code
}

3. Modified: app/Http/Controllers/CustomAuthenticatedSessionController.php

Purpose: Skip session->regenerate() and PrepareAuthenticatedSession in Docker

Changes:

  • Skip session->regenerate() call when IS_DOCKER=true (line 16)
  • Skip PrepareAuthenticatedSession in login pipeline when IS_DOCKER=true (line 35)
public function store(LoginRequest $request): LoginResponse
{
    return $this->loginPipeline($request)->then(function ($request) {
        // Skip session regeneration in Docker environment to avoid session persistence issues
        if (!env('IS_DOCKER', false)) {
            $request->session()->regenerate();
        }

        return app(LoginResponse::class);
    });
}

protected function loginPipeline(LoginRequest $request)
{
    // ... code ...

    return (new Pipeline(app()))->send($request)->through(array_filter([
        config('fortify.limiters.login') ? null : \Laravel\Fortify\Actions\EnsureLoginIsNotThrottled::class,
        config('fortify.lowercase_usernames') ? \Laravel\Fortify\Actions\CanonicalizeUsername::class : null,
        \Laravel\Fortify\Actions\AttemptToAuthenticate::class,
        // Skip PrepareAuthenticatedSession in Docker as it calls session()->migrate() which causes issues
        env('IS_DOCKER', false) ? null : \Laravel\Fortify\Actions\PrepareAuthenticatedSession::class,
    ]));
}

4. Modified: app/Http/Middleware/CheckProfileInactivity.php

Purpose: Initialize active profile session values on first request after login

Changes: Added profile initialization check after authentication verification:

public function handle(Request $request, Closure $next)
{
    // Only proceed if a user is authenticated
    if (!Auth::check()) {
        Session::forget('last_activity');
        return $next($request);
    }

    // When user is authenticated
    $activeProfileType = Session::get('activeProfileType', \App\Models\User::class);

    // Initialize active profile if not set (happens after login)
    if (!Session::has('activeProfileId')) {
        $user = Auth::guard('web')->user();
        if ($user) {
            Session::put([
                'activeProfileType'  => \App\Models\User::class,
                'activeProfileId'    => $user->id,
                'activeProfileName'  => $user->name,
                'activeProfilePhoto' => $user->profile_photo_path,
            ]);
        }
    }

    $lastActivity = Session::get('last_activity');

    // ... rest of the code
}

Why This Was Needed: The application's getActiveProfile() helper depends on activeProfileId and activeProfileType session values. These were not being set after login, only during registration. This caused the dashboard to fail with "Attempt to read property 'name' on null" error.

5. Environment Configuration: .env

Required Setting:

IS_DOCKER=true

This flag enables the custom session guard and conditional session handling.

Session Storage

The final configuration uses database sessions (SESSION_DRIVER=database), which matches the local/dev environment setup. However, the issue was not specific to the session driver - it persisted with file, Redis, and database sessions. The problem was the session migration mechanism itself.

Security Considerations

What We Skip

  • session->migrate() - Regenerating the session ID during login
  • PrepareAuthenticatedSession - Fortify's action that also calls session->migrate()

What We Keep

  • session->regenerateToken() - CSRF token is still regenerated for security
  • ConditionalAuthenticateSession middleware - Still active to validate sessions (but bypassed in Docker via middleware check)
  • Cookie encryption - Sessions are still encrypted

Production Recommendation

This solution is intended for Docker development environments only. In production:

  1. Set IS_DOCKER=false or remove the environment variable
  2. Use standard Laravel SessionGuard with full session migration
  3. Ensure proper session persistence with your production session driver

Testing

After applying these changes:

  1. Restart the app container: docker-compose restart app
  2. Navigate to login page
  3. Enter valid credentials
  4. User should be logged in and redirected to dashboard without redirect loop
  5. Session should persist across requests

Troubleshooting

If you see this error, ensure the setCookieJar($app['cookie']) line is present in AuthServiceProvider.php when creating the DockerSessionGuard.

"Attempt to read property 'name' on null" Error

This indicates the active profile session values are not being set. Ensure the changes to CheckProfileInactivity middleware are applied and the container has been restarted.

Still Getting Redirect Loop

  1. Verify IS_DOCKER=true is set in .env
  2. Check that autoloader was regenerated: docker-compose exec app composer dump-autoload
  3. Clear application cache: docker-compose exec app php artisan optimize:clear
  4. Restart container: docker-compose restart app

Alternative Solutions Attempted

Failed Approaches

  1. Disabling session encryption - Made no difference
  2. Excluding session cookie from encryption - Session data was the issue, not cookies
  3. Switching session drivers (file → Redis → database) - Issue persisted across all drivers
  4. Modifying session configuration - same_site, secure flags had no effect
  5. Custom DockerSessionGuard without cookie jar - Caused "Cookie jar has not been set" error

Why Session Migration Failed in Docker

The exact reason session migration fails in Docker while working locally is unclear, but it appears to be related to how session cookies are handled between the Docker container and the host machine during the session ID regeneration process. By keeping the original session ID and only regenerating the CSRF token, we maintain security while avoiding the persistence issue.

  • config/session.php - Session configuration (unchanged)
  • config/auth.php - Auth guards configuration (unchanged, extended at runtime)
  • app/Http/Middleware/ConditionalAuthenticateSession.php - Bypasses AuthenticateSession in Docker
  • .env - Requires IS_DOCKER=true

Summary

The fix creates a Docker-specific session guard that skips session migration during login, preventing the redirect loop while maintaining CSRF protection. The middleware also ensures active profile session values are initialized on first request after login. This allows the authentication system to work properly in Docker containers while maintaining full security in production environments.