* try { * $security = webbasics\Security::getInstance(); * * // SecureUser: can the origin of the request be trusted? * * // Verify that a user is logged in * $security->requireLogin(); * * // Use a security token to verify that the request originated from a * // trusted page. This is recommended if, for example, the script makes * // changes to the database * $security->requireToken($_REQUEST['token']); * * // Authorization: is the user allowed to request this page? * $security->requireUserRole('admin'); * * ... * * // Pass token to template so that it can be used in a submitted form or * // AJAX request * $template->set('token', $user->generateToken()); * * ... * * } catch(webbasics\SecureUserFailed $e) { * die('Get lost hacker!'); * } catch(webbasics\AuthorizationFailed $e) { * http_response_code(403); * die('You are not authorized to view this page.'); * } * * * Corresponding login controller example: * * // Find the user using ActiveRecord (not part of the WebBasics library) * $user = User::first(array('username' => $_POST['username'])); * * if (!$user) * die('Invalid username'); * * $security = webbasics\Security::getInstance(); * * // Simple: use a plain password * if (!$security->attemptPasswordLogin($user, $_POST['password'])) * die('Invalid password'); * * // More secure: hash the password in a javascript function before * // submitting the login form * if (!$security->attemptPasswordHashLogin($user, $_POST['password_hash'])) * die('Invalid password'); * * * And the User model implementation used in the example above: * * * use webbasics\ActiveRecordUser; * class User extends webbasics\ActiveRecordUser {} * * * WebBasics provides the {@link AuthenticatedUser} and {@link AuthorizedUser} * classes, which extend ActiveRecord\Model and implement both security * interfaces. * * @author Taddeus Kroes * @date 06-10-2012 * @since 0.2 * @todo Documentation, unit tests */ namespace webbasics; require_once 'session.php'; class Security implements Singleton { /** * All alphanumeric characters. * @var string */ const ALNUM_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUWXYZ01234567890123456789'; const TOKEN_NAME = 'auth_token'; const USERDATA_NAME = 'auth_userdata'; private static $instance; /** * User that is currently logged in. * @var BaseUser */ private $user; static function getInstance() { if (self::$instance === null) self::$instance = new self; return self::$instance; } private function __construct() {} function generateToken() { $session = Session::getInstance(); $token = sha1(self::generateRandomString(10)); $session->set(self::TOKEN_NAME, $token); return $token; } function requireToken($request_token) { if ($request_token != $this->getSavedToken()) throw new SecureUserFailed('invalid token "%s"', $request_token); } private function getSavedToken() { $session = Session::getInstance(); if (!$session->isRegistered(self::TOKEN_NAME)) throw new SecureUserError('no token saved in session'); return $session->get(self::TOKEN_NAME); } function requireLogin() { if ($this->user === null && !$this->loadUserFromSession()) throw new SecureUserFailed('no user is logged in'); } private function loadUserFromSession() { $session = Session::getInstance(); if (!$session->isRegistered(self::USERDATA_NAME)) return false; // Load session data $user = $session->get(self::USERDATA_NAME); // Verify session data if ($user->getSessionHash() != $this->getSessionHash()) throw new SecureUserFailure('session data could not be verified'); $this->user = $user; return true; } function getSessionHash() { return self::hash(Session::getInstance()->getId()); } function requireUserRole($required_role) { if (!($this->user instanceof RoleUser)) throw new AuthorizationError('user must implement interface RoleUser'); if ($this->user->getRole() != $required_role) throw new AuthorizationFailed('page requires user role "%s"', $required_role); } function attemptPasswordLogin(BaseUser $user, $password) { return $this->attemptPasswordHashLogin($user, self::hash($password)); } function attemptPasswordHashLogin(BaseUser $user, $password_hash) { if ($password_hash != $user->getPasswordHash()) return false; $this->loginUser($user); return true; } private function loginUser(BaseUser $user) { // Create a new session id so that a hijacked session will not become // logged in as well Session::getInstance()->regenerateId(); // Save a hash of the new session id in the database for verification $user->setSessionHash($this->getSessionHash()); $this->user = clone $user; Session::getInstance()->set(self::USERDATA_NAME, $this->user); } /** * Get the hash value of a string. * * THe hash function used is SHA-256. * * @param string $string The string to hash. * @return string A 64-byte hash. */ static function hash($string) { return hash('sha256', $string); } /** * Generate a random string of the specified length. * * @param int $lengh The length of the string to generate. * @param string $chars The caracters to choose from, defaults to {@link ALNUM_CHARS}. * @return string The generated string. */ static function generateRandomString($length, $chars=self::ALNUM_CHARS) { srand(time()); $string = ''; for ($i = 0; $i < $length; $i++) $string .= $chars[rand(0, strlen($chars) - 1)]; return $string; } } interface BaseUser { function getUsername(); function getPasswordHash(); } interface SecureUser extends BaseUser { function getSessionHash(); function setSessionHash($hash); } interface RoleUser { function getRole(); } /** * Exception, thrown when an error occurs during authentication. */ class SecureUserError extends FormattedException {} /** * Exception, thrown when the current user cannot be authenticated. */ class SecureUserFailed extends FormattedException {} /** * Exception, thrown when an error occurs during authorization. */ class AuthorizationError extends FormattedException {} /** * Exception, thrown when the current user is unauthorized to perform the * current request. */ class AuthorizationFailed extends FormattedException {} /** * The StaticUser class is meant for websites without databases, it allows for * hard-coded usernames and passwords to be specified in user objects. * * Example usage: * * class JohnDoe extends webbasics\StaticUser { * function getUsername() { * return 'john'; * } * * function getPassword() { * return 'foobar'; * } * } * * // Login controller: * $security = webbasics\Security::getInstance(); * * switch ($_POST['username']) { * case 'john': * $success = $security->attemptPasswordLogin(new JohnDoe, $_POST['password']); * break; * ... * } * * // And this might be handy during debugging: * $security->loginUser(new JohnDoe); * * * Note that this simple method of authentication even allows role-based authorization: * * abstract class User extends webbasics\StaticUser implements webbasics\RoleUser {} * * class JohnDoe extends User { * function getUsername { return 'john'; } * function getPassword { return 'foobar'; } * function getRole { return 'admin'; } * } * * class JaneDoe extends User { * function getUsername { return 'jane'; } * function getPassword { return 'barfoo'; } * function getRole { return 'member'; } * } * */ abstract class StaticUser implements BaseUser { /** * @see BaseUser::getPasswordHash() */ function getPasswordHash() { return Security::hash($this->getPassword()); } /** * * * @return string */ abstract function getPassword(); } ?>