security.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <?php
  2. /**
  3. * Security functions for user authentication and authorization.
  4. *
  5. * Usage example:
  6. * <code>
  7. * try {
  8. * $security = webbasics\Security::getInstance();
  9. *
  10. * // SecureUser: can the origin of the request be trusted?
  11. *
  12. * // Verify that a user is logged in
  13. * $security->requireLogin();
  14. *
  15. * // Use a security token to verify that the request originated from a
  16. * // trusted page. This is recommended if, for example, the script makes
  17. * // changes to the database
  18. * $security->requireToken($_REQUEST['token']);
  19. *
  20. * // Authorization: is the user allowed to request this page?
  21. * $security->requireUserRole('admin');
  22. *
  23. * ...
  24. *
  25. * // Pass token to template so that it can be used in a submitted form or
  26. * // AJAX request
  27. * $template->set('token', $user->generateToken());
  28. *
  29. * ...
  30. *
  31. * } catch(webbasics\SecureUserFailed $e) {
  32. * die('Get lost hacker!');
  33. * } catch(webbasics\AuthorizationFailed $e) {
  34. * http_response_code(403);
  35. * die('You are not authorized to view this page.');
  36. * }
  37. * </code>
  38. *
  39. * Corresponding login controller example:
  40. * <code>
  41. * // Find the user using ActiveRecord (not part of the WebBasics library)
  42. * $user = User::first(array('username' => $_POST['username']));
  43. *
  44. * if (!$user)
  45. * die('Invalid username');
  46. *
  47. * $security = webbasics\Security::getInstance();
  48. *
  49. * // Simple: use a plain password
  50. * if (!$security->attemptPasswordLogin($user, $_POST['password']))
  51. * die('Invalid password');
  52. *
  53. * // More secure: hash the password in a javascript function before
  54. * // submitting the login form
  55. * if (!$security->attemptPasswordHashLogin($user, $_POST['password_hash']))
  56. * die('Invalid password');
  57. * </code>
  58. *
  59. * And the User model implementation used in the example above:
  60. * <code>
  61. *
  62. * use webbasics\ActiveRecordUser;
  63. * class User extends webbasics\ActiveRecordUser {}
  64. * </code>
  65. *
  66. * WebBasics provides the {@link AuthenticatedUser} and {@link AuthorizedUser}
  67. * classes, which extend ActiveRecord\Model and implement both security
  68. * interfaces.
  69. *
  70. * @author Taddeus Kroes
  71. * @date 06-10-2012
  72. * @since 0.2
  73. * @todo Documentation, unit tests
  74. */
  75. namespace webbasics;
  76. require_once 'session.php';
  77. class Security implements Singleton {
  78. /**
  79. * All alphanumeric characters.
  80. * @var string
  81. */
  82. const ALNUM_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUWXYZ01234567890123456789';
  83. const TOKEN_NAME = 'auth_token';
  84. const USERDATA_NAME = 'auth_userdata';
  85. private static $instance;
  86. /**
  87. * User that is currently logged in.
  88. * @var BaseUser
  89. */
  90. private $user;
  91. static function getInstance() {
  92. if (self::$instance === null)
  93. self::$instance = new self;
  94. return self::$instance;
  95. }
  96. private function __construct() {}
  97. function generateToken() {
  98. $session = Session::getInstance();
  99. $token = sha1(self::generateRandomString(10));
  100. $session->set(self::TOKEN_NAME, $token);
  101. return $token;
  102. }
  103. function requireToken($request_token) {
  104. if ($request_token != $this->getSavedToken())
  105. throw new SecureUserFailed('invalid token "%s"', $request_token);
  106. }
  107. private function getSavedToken() {
  108. $session = Session::getInstance();
  109. if (!$session->isRegistered(self::TOKEN_NAME))
  110. throw new SecureUserError('no token saved in session');
  111. return $session->get(self::TOKEN_NAME);
  112. }
  113. function requireLogin() {
  114. if ($this->user === null && !$this->loadUserFromSession())
  115. throw new SecureUserFailed('no user is logged in');
  116. }
  117. private function loadUserFromSession() {
  118. $session = Session::getInstance();
  119. if (!$session->isRegistered(self::USERDATA_NAME))
  120. return false;
  121. // Load session data
  122. $user = $session->get(self::USERDATA_NAME);
  123. // Verify session data
  124. if ($user->getSessionHash() != $this->getSessionHash())
  125. throw new SecureUserFailure('session data could not be verified');
  126. $this->user = $user;
  127. return true;
  128. }
  129. function getSessionHash() {
  130. return self::hash(Session::getInstance()->getId());
  131. }
  132. function requireUserRole($required_role) {
  133. if (!($this->user instanceof RoleUser))
  134. throw new AuthorizationError('user must implement interface RoleUser');
  135. if ($this->user->getRole() != $required_role)
  136. throw new AuthorizationFailed('page requires user role "%s"', $required_role);
  137. }
  138. function attemptPasswordLogin(BaseUser $user, $password) {
  139. return $this->attemptPasswordHashLogin($user, self::hash($password));
  140. }
  141. function attemptPasswordHashLogin(BaseUser $user, $password_hash) {
  142. if ($password_hash != $user->getPasswordHash())
  143. return false;
  144. $this->loginUser($user);
  145. return true;
  146. }
  147. private function loginUser(BaseUser $user) {
  148. // Create a new session id so that a hijacked session will not become
  149. // logged in as well
  150. Session::getInstance()->regenerateId();
  151. // Save a hash of the new session id in the database for verification
  152. $user->setSessionHash($this->getSessionHash());
  153. $this->user = clone $user;
  154. Session::getInstance()->set(self::USERDATA_NAME, $this->user);
  155. }
  156. /**
  157. * Get the hash value of a string.
  158. *
  159. * THe hash function used is SHA-256.
  160. *
  161. * @param string $string The string to hash.
  162. * @return string A 64-byte hash.
  163. */
  164. static function hash($string) {
  165. return hash('sha256', $string);
  166. }
  167. /**
  168. * Generate a random string of the specified length.
  169. *
  170. * @param int $lengh The length of the string to generate.
  171. * @param string $chars The caracters to choose from, defaults to {@link ALNUM_CHARS}.
  172. * @return string The generated string.
  173. */
  174. static function generateRandomString($length, $chars=self::ALNUM_CHARS) {
  175. srand(time());
  176. $string = '';
  177. for ($i = 0; $i < $length; $i++)
  178. $string .= $chars[rand(0, strlen($chars) - 1)];
  179. return $string;
  180. }
  181. }
  182. interface BaseUser {
  183. function getUsername();
  184. function getPasswordHash();
  185. }
  186. interface SecureUser extends BaseUser {
  187. function getSessionHash();
  188. function setSessionHash($hash);
  189. }
  190. interface RoleUser {
  191. function getRole();
  192. }
  193. /**
  194. * Exception, thrown when an error occurs during authentication.
  195. */
  196. class SecureUserError extends FormattedException {}
  197. /**
  198. * Exception, thrown when the current user cannot be authenticated.
  199. */
  200. class SecureUserFailed extends FormattedException {}
  201. /**
  202. * Exception, thrown when an error occurs during authorization.
  203. */
  204. class AuthorizationError extends FormattedException {}
  205. /**
  206. * Exception, thrown when the current user is unauthorized to perform the
  207. * current request.
  208. */
  209. class AuthorizationFailed extends FormattedException {}
  210. /**
  211. * The StaticUser class is meant for websites without databases, it allows for
  212. * hard-coded usernames and passwords to be specified in user objects.
  213. *
  214. * Example usage:
  215. * <code>
  216. * class JohnDoe extends webbasics\StaticUser {
  217. * function getUsername() {
  218. * return 'john';
  219. * }
  220. *
  221. * function getPassword() {
  222. * return 'foobar';
  223. * }
  224. * }
  225. *
  226. * // Login controller:
  227. * $security = webbasics\Security::getInstance();
  228. *
  229. * switch ($_POST['username']) {
  230. * case 'john':
  231. * $success = $security->attemptPasswordLogin(new JohnDoe, $_POST['password']);
  232. * break;
  233. * ...
  234. * }
  235. *
  236. * // And this might be handy during debugging:
  237. * $security->loginUser(new JohnDoe);
  238. * </code>
  239. *
  240. * Note that this simple method of authentication even allows role-based authorization:
  241. * <code>
  242. * abstract class User extends webbasics\StaticUser implements webbasics\RoleUser {}
  243. *
  244. * class JohnDoe extends User {
  245. * function getUsername { return 'john'; }
  246. * function getPassword { return 'foobar'; }
  247. * function getRole { return 'admin'; }
  248. * }
  249. *
  250. * class JaneDoe extends User {
  251. * function getUsername { return 'jane'; }
  252. * function getPassword { return 'barfoo'; }
  253. * function getRole { return 'member'; }
  254. * }
  255. * </code>
  256. */
  257. abstract class StaticUser implements BaseUser {
  258. /**
  259. * @see BaseUser::getPasswordHash()
  260. */
  261. function getPasswordHash() {
  262. return Security::hash($this->getPassword());
  263. }
  264. /**
  265. *
  266. *
  267. * @return string
  268. */
  269. abstract function getPassword();
  270. }
  271. ?>