148 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			148 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
/**
 | 
						|
 * Security functions
 | 
						|
 */
 | 
						|
class Security {
 | 
						|
 | 
						|
    /** @var int Run as a single user requiring no password */
 | 
						|
    public const int SINGLE_USER = 0;
 | 
						|
 | 
						|
    /** @var int Run as a single user requiring a password */
 | 
						|
    public const int SINGLE_USER_WITH_PASSWORD = 1;
 | 
						|
 | 
						|
    /** @var int Require users to provide e-mail address and password */
 | 
						|
    public const int MULTI_USER = 2;
 | 
						|
 | 
						|
    /** @var string The e-mail address for the single user */
 | 
						|
    public const string SINGLE_USER_EMAIL = 'solouser@example.com';
 | 
						|
 | 
						|
    /** @var string The password for the single user with no password */
 | 
						|
    public const string SINGLE_USER_PASSWORD = 'no-password-required';
 | 
						|
 | 
						|
    /** @var string The password algorithm to use for our passwords */
 | 
						|
    public const string PW_ALGORITHM = PASSWORD_DEFAULT;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Find a user by their ID
 | 
						|
     *
 | 
						|
     * @param string $email The e-mail address of the user to retrieve
 | 
						|
     * @param SQLite3 $db The data connection to use to retrieve the user
 | 
						|
     * @return array|false The user information, or null if the user is not found
 | 
						|
     */
 | 
						|
    public static function findUserByEmail(string $email, SQLite3 $db): array|false {
 | 
						|
        $query = $db->prepare('SELECT * FROM frc_user WHERE email = :email');
 | 
						|
        $query->bindValue(':email', $email);
 | 
						|
        $result = $query->execute();
 | 
						|
        return $result ? $result->fetchArray(SQLITE3_ASSOC) : false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Add a user
 | 
						|
     *
 | 
						|
     * @param string $email The e-mail address for the user
 | 
						|
     * @param string $password The user's password
 | 
						|
     * @param SQLite3 $db The data connection to use to add the user
 | 
						|
     */
 | 
						|
    public static function addUser(string $email, string $password, SQLite3 $db): void {
 | 
						|
        $query = $db->prepare('INSERT INTO frc_user (email, password) VALUES (:email, :password)');
 | 
						|
        $query->bindValue(':email',    $email);
 | 
						|
        $query->bindValue(':password', password_hash($password, self::PW_ALGORITHM));
 | 
						|
        $query->execute();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Verify a user's password
 | 
						|
     *
 | 
						|
     * @param array $user The user information retrieved from the database
 | 
						|
     * @param string $password The password provided by the user
 | 
						|
     * @param string|null $returnTo The URL to which the user should be redirected
 | 
						|
     * @param SQLite3 $db The database connection to use to verify the user's credentials
 | 
						|
     */
 | 
						|
    private static function verifyPassword(array $user, string $password, ?string $returnTo, SQLite3 $db): void {
 | 
						|
        if (password_verify($password, $user['password'])) {
 | 
						|
            if (password_needs_rehash($user['password'], self::PW_ALGORITHM)) {
 | 
						|
                $rehash = $db->prepare('UPDATE frc_user SET password = :hash WHERE id = :id');
 | 
						|
                $rehash->bindValue(':hash', password_hash($password, self::PW_ALGORITHM));
 | 
						|
                $rehash->bindValue(':id',   $user['id']);
 | 
						|
                $rehash->execute();
 | 
						|
            }
 | 
						|
            $_SESSION[Key::USER_ID]    = $user['id'];
 | 
						|
            $_SESSION[Key::USER_EMAIL] = $user['email'];
 | 
						|
            frc_redirect($returnTo ?? '/');
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Log on a user with e-mail address and password
 | 
						|
     *
 | 
						|
     * @param string $email The e-mail address for the user (cannot be the single-user mode user)
 | 
						|
     * @param string $password The password provided by the user
 | 
						|
     * @param string|null $returnTo The URL to which the user should be redirected
 | 
						|
     * @param SQLite3 $db The database connection to use to verify the user's credentials
 | 
						|
     */
 | 
						|
    public static function logOnUser(string $email, string $password, ?string $returnTo, SQLite3 $db): void {
 | 
						|
        if (SECURITY_MODEL == self::SINGLE_USER_WITH_PASSWORD) {
 | 
						|
            $dbEmail = self::SINGLE_USER_EMAIL;
 | 
						|
        } else {
 | 
						|
            if ($email == self::SINGLE_USER_EMAIL) {
 | 
						|
                add_error('Invalid credentials; log on unsuccessful');
 | 
						|
                return;
 | 
						|
            }
 | 
						|
            $dbEmail = $email;
 | 
						|
        }
 | 
						|
        $user = self::findUserByEmail($dbEmail, $db);
 | 
						|
        if ($user) self::verifyPassword($user, $password, $returnTo, $db);
 | 
						|
        add_error('Invalid credentials; log on unsuccessful');
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Update the password for the given user
 | 
						|
     *
 | 
						|
     * @param string $email The e-mail address of the user whose password should be updated
 | 
						|
     * @param string $password The new password for this user
 | 
						|
     * @param SQLite3 $db The database connection to use in updating the password
 | 
						|
     */
 | 
						|
    public static function updatePassword(string $email, string $password, SQLite3 $db): void {
 | 
						|
        $query = $db->prepare('UPDATE frc_user SET password = :password WHERE email = :email');
 | 
						|
        $query->bindValue(':password', password_hash($password, self::PW_ALGORITHM));
 | 
						|
        $query->bindValue(':email',    $email);
 | 
						|
        $query->execute();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Log on the single user
 | 
						|
     *
 | 
						|
     * @param SQLite3 $db The data connection to use to retrieve the user
 | 
						|
     */
 | 
						|
    private static function logOnSingleUser(SQLite3 $db): void {
 | 
						|
        $user = self::findUserByEmail(self::SINGLE_USER_EMAIL, $db);
 | 
						|
        if (!$user) {
 | 
						|
            self::addUser(self::SINGLE_USER_EMAIL, self::SINGLE_USER_PASSWORD, $db);
 | 
						|
            $user = self::findUserByEmail(self::SINGLE_USER_EMAIL, $db);
 | 
						|
        }
 | 
						|
        self::verifyPassword($user, self::SINGLE_USER_PASSWORD, $_GET['returnTo'], $db);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Verify that user is logged on
 | 
						|
     *
 | 
						|
     * @param SQLite3 $db The data connection to use if required
 | 
						|
     * @param bool $redirectIfAnonymous Whether to redirect the request if there is no user logged on
 | 
						|
     */
 | 
						|
    public static function verifyUser(SQLite3 $db, bool $redirectIfAnonymous = true): void {
 | 
						|
        if (key_exists(Key::USER_ID, $_SESSION)) return;
 | 
						|
 | 
						|
        if (SECURITY_MODEL == self::SINGLE_USER) self::logOnSingleUser($db);
 | 
						|
 | 
						|
        if (SECURITY_MODEL != self::SINGLE_USER_WITH_PASSWORD && SECURITY_MODEL != self::MULTI_USER) {
 | 
						|
            die('Unrecognized security model (' . SECURITY_MODEL . ')');
 | 
						|
        }
 | 
						|
 | 
						|
        if ($redirectIfAnonymous) {
 | 
						|
            header("Location: /user/log-on?returnTo={$_SERVER['REQUEST_URI']}", true, 307);
 | 
						|
            die();
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |