Implementing "Remember Me" on a Website

Even though most modern browsers can remember your usernames and passwords for you, it is nice for a web application to allow an individual to return to a web site and be automatically logged in without having to submit credentials every time they do so. Implementing "Remember Me" with the Istarel Workshop Application Framework requires several code blocks, and this article assumes that basic security has already been implemented in the application.

Update the Login Form

In the Login application module, you need to add a checkbox element.

Partial Listing: Login.php

class Login extends ApplicationModule
{
    ...

    function formElements()
    {
        return array(
            'header'        => new IWHeaderElement('Login Information'),
            'email'         => new IWFormElement('Email:', new IWTextElement(30, 100)),
            'password'     => new IWFormElement('Password:', new IWPasswordElement(10, 12)),
            'remember_me' => new IWFormElement(null, new IWCheckboxElement, 'Remember Me'),
        );
    }

    ...
}

Update the Login Validation

As a reminder, the ApplicationSecurityDelegate is invoked for handling login, called via its validateLogin() method. The approach I use is for the applicable user model object factory class to handle actual authentication (using $_POST as the input data). If the authentication succeeds, a cookie is established.

Partial Listing: rsrc/ApplicationSecurityDelegate.php

class ApplicationSecurityDelegate
{
    const COOKIE_DURATION = 31536000;    // one year

    function validateLogin($form)
    {
        // If a valid user results from authentication, login is valid
        if ($this->user = AppUserFactory::authenticatedUser($_POST))
        {
            // If "remember me" is checked, set up a cookie
            if (IWRequest::postValue('remember_me', null)) $this->establishCookie();

            return null;
        }

        // Failed validation should result in a field-keyed array of errors
        return array('password' => 'Invalid email address or password');
    }

    function establishCookie()
    {
        if (! $this->user) return;

        // Establish the cookie
        setcookie(
            AppUser::IDENTIFIER,
            md5($this->user->app_user_id),
            time() + ApplicationSecurityDelegate::COOKIE_DURATION,
            APPL_ROOT_DIR
        );
    }

    ...

Update User Instantiation

The ApplicationSecurityDelegate is a singleton, and its instantiation must be modified to accommodate the possilbility of a cookie-based user (much like the original change to look in the session variable).

Partial Listing: rsrc/ApplicationSecurityDelegate.php

class ApplicationSecurityDelegate

    protected function __construct() { }

    protected $user;

    static function sharedApplicationSecurityDelegate()
    {
        static $instance;
        if (! $instance) $instance = new ApplicationSecurityDelegate;
        $instance->initFromSession();

        if (! $instance->user()) $instance->initFromCookie();

        return $instance;
    }

    function initFromSession()
    {
        if (isset($_SESSION['USER'])) $this->user = $_SESSION['USER'];
    }

    function initFromCookie()
    {
        // If there is no cookie, do nothing
        if (! isset($_COOKIE[AppUser::IDENTIFIER])) return;

        // Use the cookie data to establish the current user
        $this->user = AppUserFactory::retrieveAppUser($_COOKIE[AppUser::IDENTIFIER],
                                                    'md5(app_user_id::text)');

        // If no user could be established, do nothing
        if (! $this->user) return;

        // Establish the current session and update the cookie
        $this->establishSession();
        $this->establishCookie();
    }

Implement the Authentication method

The authenticatedUser() static method on AppUserFactory must consider whether the "Remember Me" state is active. If the checkbox has been checked, then a cookie is set if authentication succeeds. AppUser is included to show the identifier used in the cookie.

Partial Listing: rsrc/model/application/AppUser.php and rsrc/model/application/AppUserFactory.php

class AppUser extends DefaultAppUser
{
    // used by the ApplicationSecurityDelegate
    const IDENTIFIER = 'iwid';

    ...
}

class AppUserFactory extends DefaultAppUserFactory
{
    ...

    static function authenticatedUser($data = null)
    {
        // If no credentials provided, authentication fails
        if (! $data) return null;

        // If no password provided, authentication fails
        if (! isset($data['password']) or ! $data['password']) return false;

        // Retrieve an AppUser object using the credentials
        $login = AppUserFactory::retrieveAppUser($credentials['email'], 'lower(email)');

        // If no AppUser object could be created, authentication fails
        if (! $login) return false;

        // If the password does not match, authentication fails
        if ($login->password != md5($data['password'])) return false;

        // Return the authenticated user
        return $login;
    }

    ...
}

Word of Warning

Obviously, you want to use this technology with discretion, since it performs no authentication after the individuals logs in for the first time. That's probably fine for a user on their home or work computer, but someone clicking the "Remember Me" checkbox on a public computer exposes the application to anyone that happens to wander through the browser history.