Skip to content

Bug: Remember-me token refresh fails in loginView() - missing ->withCookies() on redirect #1305

Closed

Description

@tomatlscomm

PHP Version

8.3

CodeIgniter4 Version

4.6.3

Shield Version

1.2.0

Which operating systems have you tested for this bug?

macOS

Which server did you use?

apache

Database

10.11.13-MariaDB

Did you customize Shield?

No

What happened?

[Note : i used Claude Code to redact the text below, bug the issue has been traced by a human]

When a user with a valid remember-me cookie accesses the login page, the loginView() method in LoginController redirects already-logged-in users without preserving cookies.

Bug Location:
vendor/codeigniter4/shield/src/Controllers/LoginController.php:34

Current Code:

  public function loginView()
  {
      if (auth()->loggedIn()) {
          return redirect()->to(config('Auth')->loginRedirect());  // ❌ Missing ->withCookies()
      }
      // ...
  }

Expected Code:

  public function loginView()
  {
      if (auth()->loggedIn()) {
          return redirect()->to(config('Auth')->loginRedirect())->withCookies();  // ✅ Fixed
      }
      // ...
  }

Impact:

When Session::checkRememberMe() validates a remember-me token, it calls refreshRememberMeToken() which:

  1. Generates a new validator
  2. Updates the hashed validator in database
  3. Sends a new cookie via setRememberMeCookie()

However, the redirect at line 34 doesn't include ->withCookies(), so the new cookie is lost. The browser keeps the old cookie with the old validator, which no longer matches the updated hash in the database.

On the next visit, authentication fails with:
hash_equals($token->hashedValidator, $hashedValidator) === false // Session.php:631

Steps to Reproduce

  1. Enable remember-me: $sessionConfig['allowRemembering'] = true
  2. Login with remember-me checkbox checked
  3. Wait for token refresh (or clear session to trigger remember-me authentication)
  4. Access login page → redirect happens but new cookie is lost
  5. Next visit → authentication fails because cookie validator doesn't match database hash

Expected Output

Expected Code:

  public function loginView()
  {
      if (auth()->loggedIn()) {
          return redirect()->to(config('Auth')->loginRedirect())->withCookies();  // ✅ Fixed
      }
      // ...
  }

=> The token will be refreshed in the cookie

Anything else?

No response

Activity

  1. datamweb commented on Jan 8, 2026

    @datamweb
    Collaborator

    Great catch!
    Since the redirect() creates a new Response instance, any cookies set in the global response (like the refreshed remember-me token) are indeed dropped unless withCookies() is explicitly called.

    Would you be open to submitting a Pull Request for this fix?

  2. tomatlscomm commented on Jan 8, 2026

    @tomatlscomm
    ContributorAuthor

    Done

  3. LenoilR commented on Jan 26, 2026

    @LenoilR

    Great find—but you're just scratching the surface of the problem. Any CodeIgniter project that uses redirects and Shield is susceptible to the same issue (I've had this problem for two years in my site and couldn't figure it out until you posted your bug report). Rather than requiring every redirect to call withCookies() just in case a new remember me cookie needs to be sent, the better choice is for Shield to automatically add updated remember me cookies to redirect responses. This can be done by Shield replacing CodeIgniter's 'redirectresponse' service. This requires two edits to vendor/codeigniter4/shield/src/Config/Services.php:

    1. Add these additional use statements before the Services class definition:
    use Config\Services as AppServices;
    use Config\App;
    use CodeIgniter\HTTP\RedirectResponse;
    
    
    1. Add this additional method to the Services class definition:
        public static function redirectresponse(?App $config = null, bool $getShared = true)
        {
            if ($getShared) {
                return static::getSharedInstance('redirectresponse', $config);
            }
    
            $config ??= config(App::class);
            $response = new RedirectResponse($config);
            $response->setProtocolVersion(AppServices::request()->getProtocolVersion());
    
            // If a remember me cookie is set in the global response, copy it to the redirect response
            $rememberCookie = AppServices::response($config)->getCookie(
                setting('Auth.sessionConfig')['rememberCookieName']
            );
            if ($rememberCookie) {
                $response->setCookie($rememberCookie);
            }
    
            return $response;
        }
    
    

    The above fix automatically adds the new remember me cookie, if present, to all redirect responses. Your proposed fix to Shield's loginView would not be needed.

  4. tomatlscomm commented on Jan 26, 2026

    @tomatlscomm
    ContributorAuthor

    Hi.

    Thank you for this complement.
    Here is a more "generalist" approach with does not limit to "shield" cookies and avoid to edit core files :

    1. Create a file app/HTTP/RedirectResponse.php

    <?php
    
    namespace App\HTTP;
    
    use CodeIgniter\HTTP\RedirectResponse as BaseRedirectResponse;
    
    class RedirectResponse extends BaseRedirectResponse
    {
    
        public function send()
        {
            // Automatiquement copier les cookies de la Response globale
            $this->withCookies();
    
            return parent::send();
        }
    }
    

    2. Update file app/Config/Services.php

    <?php
    
    namespace Config;
    
    use CodeIgniter\Config\BaseService;
    use Config\App;
    use App\HTTP\RedirectResponse;
    
    /**
     * Services Configuration file.
     *
     * Services are simply other classes/libraries that the system uses
     * to do its job. This is used by CodeIgniter to allow the core of the
     * framework to be swapped out easily without affecting the usage within
     * the rest of your application.
     *
     * This file holds any application-specific services, or service overrides
     * that you might need. An example has been included with the general
     * method format you should use for your service methods. For more examples,
     * see the core Services file at system/Config/Services.php.
     */
    class Services extends BaseService
    {
        public static function redirectresponse(?App $config = null, bool $getShared = true)
        {
            if ($getShared) {
                return static::getSharedInstance('redirectresponse', $config);
            }
    
            $config ??= config(App::class);
            $response = new RedirectResponse($config);
            $response->setProtocolVersion(static::request()->getProtocolVersion());
    
            return $response;
        }
    }
    

    Then, any redirect will automaticly be populated with cookies if needed.

  5. LenoilR commented on Feb 6, 2026

    @LenoilR

    While my code worked, I just noticed that it fills up the log with stack traces, because getCookie() logs whenever the cookie it's looking for isn't found. So instead of calling getCookie() and checking that the result isn't null, call hasCookie() and check that. Here's the revised redirectresponse method:

        public static function redirectresponse(?App $config = null, bool $getShared = true)
        {
            if ($getShared) {
                return static::getSharedInstance('redirectresponse', $config);
            }
    
            $config ??= config(App::class);
            $response = new RedirectResponse($config);
            $response->setProtocolVersion(request()->getProtocolVersion());
    
            // If a remember me cookie is set in the global response, copy it to the redirect response
            $cookieName = setting('Auth.sessionConfig')['rememberCookieName'];
            if (static::response($config)->hasCookie($cookieName)) {
                $response->setCookie(static::response($config)->getCookie($cookieName));
            }
        
            return $response;
        }
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions