lonnieezell / myth-auth

One-stop Auth package for CodeIgniter 4
MIT License
632 stars 207 forks source link

ASK Invalid cookie expiration time after setting allow Remember me to true #611

Closed yseStudio closed 9 months ago

yseStudio commented 9 months ago

[ASK] I'm still new to the world of codeigniter 4, I have set allowRememberme =true; but I keep getting the error log "Invalid cookie expiration time." and I don't think I changed anything in Auth/Config in public $rememberLength = 30 * DAY; Is there something wrong with the default myth-auth code below? Can anyone explain??

$appConfig = config('Cookie');
        $response  = service('response');

        // Create the cookie
        $response->setCookie(
            'remember',                                 // Cookie Name
            $this->config->rememberLength,      // # Seconds until it expires
            $appConfig->domain,
            $appConfig->path,
            $appConfig->prefix,
            $appConfig->secure,                 // Only send over HTTPS?
            true                                // Hide from Javascript?
        );
        $response->send()
manageruz commented 9 months ago

If you're using latest CI, current myth auth needs some updates to work with it. Because from CI's v4.4.0 it's modified the structure of config files.

manageruz commented 9 months ago

I already checked dev version with CI v4.4.3 and there is no "invalid cookie expiration time" error. What is your CodeIgniter and myth auth version?

manageruz commented 9 months ago

And just check the value of $this->config->rememberLength parameter in your code.

yseStudio commented 9 months ago

thank you @manageruz for answering my question, I am using CI version 4.4.3 and myth auth v1.2.1, I have read the documentation and did not find any guidance for this error, and I have changed the Config Cookie which has been adjusted in this version, but it is still the same,

$appConfig = config('Cookie');
        $response  = service('response');

        // Create the cookie
        $response->setCookie(
            'remember',                                 // Cookie Name
            $this->config->rememberLength,      // # Seconds until it expires
            $appConfig->domain,
            $appConfig->path,
            $appConfig->prefix,
            $appConfig->secure,                 // Only send over HTTPS?
            true                                // Hide from Javascript?
        );
        $response->send();

Do you have an example of auth and cookie configuration code that has been tested and is successful?

manageruz commented 9 months ago

Ok, for now please update your myth auth to the latest dev version. Then in your CI's .env file set baseUrl, CI_ENVIROMENT = development and database, cookie, security and session values according to your data.


Sample database settings:
 database.default.hostname = localhost
 database.default.database = ci441
 database.default.username = root
 database.default.password = 
 database.default.DBDriver = MySQLi
 database.default.DBPrefix =
 database.default.port = 3306

Sample cookie settings:
 cookie.prefix = ''
 cookie.expires = 0
 cookie.path = '/'
 cookie.domain = ''
 cookie.secure = false
 cookie.httponly = false
 cookie.samesite = 'Lax'
 cookie.raw = false

Sample security settings:
 security.csrfProtection = 'cookie'
 security.tokenRandomize = false
 security.tokenName = 'csrf_token_name'
 security.headerName = 'X-CSRF-TOKEN'
 security.cookieName = 'csrf_cookie_name'
 security.expires = 7200
 security.regenerate = true
 security.redirect = false
 security.samesite = 'Lax'

Sample session settings:
 session.driver = 'CodeIgniter\Session\Handlers\DatabaseHandler'
 session.cookieName = 'ci_session'
 session.expiration = 7200
 session.savePath = ci_sessions
 session.matchIP = false
 session.timeToUpdate = 300
 session.regenerateDestroy = false
manageruz commented 9 months ago

And this is working AuthenticationBase.php file:


<?php

namespace Myth\Auth\Authentication;

use CodeIgniter\CodeIgniter;
use CodeIgniter\Events\Events;
use CodeIgniter\Model;
use Exception;
use Myth\Auth\Config\Auth as AuthConfig;
use Myth\Auth\Entities\User;
use Myth\Auth\Exceptions\AuthException;
use Myth\Auth\Exceptions\UserNotFoundException;
use Myth\Auth\Models\LoginModel;

class AuthenticationBase
{
    /**
     * @var User|null
     */
    protected $user;

    /**
     * @var Model
     */
    protected $userModel;

    /**
     * @var LoginModel
     */
    protected $loginModel;

    /**
     * @var string
     */
    protected $error;

    /**
     * @var AuthConfig
     */
    protected $config;

    public function __construct($config)
    {
        $this->config = $config;
    }

    /**
     * Returns the current error, if any.
     *
     * @return string
     */
    public function error()
    {
        return $this->error;
    }

    /**
     * Whether to continue instead of throwing exceptions,
     * as defined in config.
     *
     * @return bool
     */
    public function silent()
    {
        return (bool) $this->config->silent;
    }

    /**
     * Logs a user into the system.
     * NOTE: does not perform validation. All validation should
     * be done prior to using the login method.
     *
     * @param User $user
     *
     * @throws Exception
     */
    public function login(?User $user = null, bool $remember = false): bool
    {
        if (empty($user)) {
            $this->user = null;

            return false;
        }

        $this->user = $user;

        // Always record a login attempt
        $ipAddress = service('request')->getIPAddress();
        $this->recordLoginAttempt($user->email, $ipAddress, $user->id ?? null, true);

        // Regenerate the session ID to help protect against session fixation
        if (ENVIRONMENT !== 'testing') {
            session()->regenerate();
        }

        // Let the session know we're logged in
        session()->set('logged_in', $this->user->id);

        // When logged in, ensure cache control headers are in place
        service('response')->noCache();

        if ($remember && $this->config->allowRemembering) {
            $this->rememberUser($this->user->id);
        }

        // We'll give a 20% chance to need to do a purge since we
        // don't need to purge THAT often, it's just a maintenance issue.
        // to keep the table from getting out of control.
        if (random_int(1, 100) < 20) {
            $this->loginModel->purgeOldRememberTokens();
        }

        // trigger login event, in case anyone cares
        Events::trigger('login', $user);

        return true;
    }

    /**
     * Checks to see if the user is logged in.
     */
    public function isLoggedIn(): bool
    {
        // On the off chance
        if ($this->user instanceof User) {
            return true;
        }

        if ($userID = session('logged_in')) {
            // Store our current user object
            $this->user = $this->userModel->find($userID);

            return $this->user instanceof User;
        }

        return false;
    }

    /**
     * Logs a user into the system by their ID.
     */
    public function loginByID(int $id, bool $remember = false)
    {
        $user = $this->retrieveUser(['id' => $id]);

        if (empty($user)) {
            throw UserNotFoundException::forUserID($id);
        }

        return $this->login($user, $remember);
    }

    /**
     * Logs a user out of the system.
     */
    public function logout()
    {
        helper('cookie');

        // Destroy the session data - but ensure a session is still
        // available for flash messages, etc.
        if (isset($_SESSION)) {
            foreach (array_keys($_SESSION) as $key) {
                $_SESSION[$key] = null;
                unset($_SESSION[$key]);
            }
        }

        // Regenerate the session ID for a touch of added safety.
        session()->regenerate(true);

        // Remove the cookie
        delete_cookie('remember');

        // Handle user-specific tasks
        if ($user = $this->user()) {
            // Take care of any remember me functionality
            $this->loginModel->purgeRememberTokens($user->id);

            // Trigger logout event
            Events::trigger('logout', $user);

            $this->user = null;
        }
    }

    /**
     * Record a login attempt
     *
     * @return bool|int|string
     */
    public function recordLoginAttempt(string $email, ?string $ipAddress, ?int $userID, bool $success)
    {
        return $this->loginModel->insert([
            'ip_address' => $ipAddress,
            'email'      => $email,
            'user_id'    => $userID,
            'date'       => date('Y-m-d H:i:s'),
            'success'    => (int) $success,
        ]);
    }

    /**
     * Generates a timing-attack safe remember me token
     * and stores the necessary info in the db and a cookie.
     *
     * @see https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence
     *
     * @throws Exception
     */
    public function rememberUser(int $userID)
    {
        $selector  = bin2hex(random_bytes(12));
        $validator = bin2hex(random_bytes(20));
        $expires   = date('Y-m-d H:i:s', time() + $this->config->rememberLength);

        $token = $selector . ':' . $validator;

        // Store it in the database
        $this->loginModel->rememberUser($userID, $selector, hash('sha256', $validator), $expires);

        // Save it to the user's browser in a cookie.
        $appConfig = config('App');        
        $response  = service('response');

        //replace cookie config values from cookie.php file on new versions of CI (v4.4.0 and above)
        if (version_compare(CodeIgniter::CI_VERSION, '4.3.8', '>')) 
        {
            $cookieConfig = config('Cookie');

            $appConfig->cookieDomain    = $cookieConfig->domain;
            $appConfig->cookiePath      = $cookieConfig->path;
            $appConfig->cookiePrefix    = $cookieConfig->prefix;
            $appConfig->cookieSecure    = $cookieConfig->secure;
            $appConfig->cookieHTTPOnly  = $cookieConfig->httponly;
        }

        // Create the cookie
        $response->setCookie(
            'remember',                                 // Cookie Name
            $token,                                     // Value
            $this->config->rememberLength,              // # Seconds until it expires
            $appConfig->cookieDomain,
            $appConfig->cookiePath,
            $appConfig->cookiePrefix,
            $appConfig->cookieSecure,                   // Only send over HTTPS?        
            true                                        // Hide from Javascript?
        );
    }

    /**
     * Sets a new validator for this user/selector. This allows
     * a one-time use of remember-me tokens, but still allows
     * a user to be remembered on multiple browsers/devices.
     */
    public function refreshRemember(int $userID, string $selector)
    {
        $existing = $this->loginModel->getRememberToken($selector);

        // No matching record? Shouldn't happen, but remember the user now.
        if (empty($existing)) {
            return $this->rememberUser($userID);
        }

        // Update the validator in the database and the session
        $validator = bin2hex(random_bytes(20));

        $this->loginModel->updateRememberValidator($selector, $validator);

        // Save it to the user's browser in a cookie.
        helper('cookie');

        $appConfig = config('App');

        //replace cookie config values from cookie.php file on new versions of CI (v4.4.0 and above)
        if (version_compare(CodeIgniter::CI_VERSION, '4.3.8', '>')) 
        {
            $cookieConfig = config('Cookie');

            $appConfig->cookieDomain    = $cookieConfig->domain;
            $appConfig->cookiePath      = $cookieConfig->path;
            $appConfig->cookiePrefix    = $cookieConfig->prefix;
            $appConfig->cookieSecure    = $cookieConfig->secure;
            $appConfig->cookieHTTPOnly  = $cookieConfig->httponly;
        }

        // Create the cookie
        set_cookie(
            'remember',                             // Cookie Name
            $selector . ':' . $validator,               // Value
            (string) $this->config->rememberLength, // # Seconds until it expires
            $appConfig->cookieDomain,
            $appConfig->cookiePath,
            $appConfig->cookiePrefix,
            $appConfig->cookieSecure,               // Only send over HTTPS?
            true                                    // Hide from Javascript?
        );
    }

    /**
     * Returns the User ID for the current logged in user.
     *
     * @return int|null
     */
    public function id()
    {
        return $this->user->id ?? null;
    }

    /**
     * Returns the User instance for the current logged in user.
     *
     * @return User|null
     */
    public function user()
    {
        return $this->user;
    }

    /**
     * Grabs the current user from the database.
     *
     * @return array|object|null
     */
    public function retrieveUser(array $wheres)
    {
        if (! $this->userModel instanceof Model) {
            throw AuthException::forInvalidModel('User');
        }

        return $this->userModel
            ->where($wheres)
            ->first();
    }

    //--------------------------------------------------------------------
    // Model Setters
    //--------------------------------------------------------------------

    /**
     * Sets the model that should be used to work with
     * user accounts.
     *
     * @return $this
     */
    public function setUserModel(Model $model)
    {
        $this->userModel = $model;

        return $this;
    }

    /**
     * Sets the model that should be used to record
     * login attempts (but failed and successful).
     *
     * @param LoginModel $model
     *
     * @return $this
     */
    public function setLoginModel(Model $model)
    {
        $this->loginModel = $model;

        return $this;
    }
}
manageruz commented 9 months ago

As you can notice difference from latest dev version is just three. First at the top of the file added: use CodeIgniter\CodeIgniter;

After updated to methods: rememberUser and refreshRemember . Added this extra code for BC:


//replace cookie config values from cookie.php file on new versions of CI (v4.4.0 and above)
if (version_compare(CodeIgniter::CI_VERSION, '4.3.8', '>')) 
{
        $cookieConfig = config('Cookie');

        $appConfig->cookieDomain    = $cookieConfig->domain;
        $appConfig->cookiePath      = $cookieConfig->path;
        $appConfig->cookiePrefix    = $cookieConfig->prefix;
        $appConfig->cookieSecure    = $cookieConfig->secure;
        $appConfig->cookieHTTPOnly  = $cookieConfig->httponly;
}
manageruz commented 9 months ago

And for simplicity sample auth.php config file some parts:


public $requireActivation = null;
public $allowRemembering = true;
public $rememberLength = 30 * DAY;
yseStudio commented 9 months ago

thank you very much @manajeruz for all the samples you provided, I will try it tonight, hope it works, thank you very much 🔥🔥🔥

yseStudio commented 9 months ago

thank you @manajeruz for taking the time to answer my question, I have implemented it and it works. thank you very much @manajeruz