aboutsummaryrefslogtreecommitdiffhomepage
path: root/main/app/sprinkles/account/src/Authenticate
diff options
context:
space:
mode:
authorMarvin Borner2018-06-08 20:03:25 +0200
committerMarvin Borner2018-06-08 20:03:25 +0200
commit92b7dd3335a6572debeacfb5faa82c63a5e67888 (patch)
tree7ebbca22595d542ec5e2912a24a0400ac8f6b113 /main/app/sprinkles/account/src/Authenticate
parent22a1bb27f94ea33042b0bdd35bef1a5cfa96cc0d (diff)
Some minor fixes
Diffstat (limited to 'main/app/sprinkles/account/src/Authenticate')
-rw-r--r--main/app/sprinkles/account/src/Authenticate/AuthGuard.php110
-rw-r--r--main/app/sprinkles/account/src/Authenticate/Authenticator.php814
-rw-r--r--main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php44
-rw-r--r--main/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php44
-rw-r--r--main/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php44
-rw-r--r--main/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php42
-rw-r--r--main/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php44
-rw-r--r--main/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php44
-rw-r--r--main/app/sprinkles/account/src/Authenticate/Hasher.php210
9 files changed, 698 insertions, 698 deletions
diff --git a/main/app/sprinkles/account/src/Authenticate/AuthGuard.php b/main/app/sprinkles/account/src/Authenticate/AuthGuard.php
index ce64bd7..9603a87 100644
--- a/main/app/sprinkles/account/src/Authenticate/AuthGuard.php
+++ b/main/app/sprinkles/account/src/Authenticate/AuthGuard.php
@@ -1,55 +1,55 @@
-<?php
-/**
- * UserFrosting (http://www.userfrosting.com)
- *
- * @link https://github.com/userfrosting/UserFrosting
- * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
- */
-
-namespace UserFrosting\Sprinkle\Account\Authenticate;
-
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\ServerRequestInterface;
-use Slim\Http\Body;
-use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException;
-
-/**
- * Middleware to catch requests that fail because they require user authentication.
- *
- * @author Alex Weissman (https://alexanderweissman.com)
- */
-class AuthGuard
-{
- /**
- * @var Authenticator
- */
- protected $authenticator;
-
- /**
- * Constructor.
- *
- * @param $authenticator Authenticator The current authentication object.
- */
- public function __construct($authenticator) {
- $this->authenticator = $authenticator;
- }
-
- /**
- * Invoke the AuthGuard middleware, throwing an exception if there is no authenticated user in the session.
- *
- * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request
- * @param \Psr\Http\Message\ResponseInterface $response PSR7 response
- * @param callable $next Next middleware
- *
- * @return \Psr\Http\Message\ResponseInterface
- */
- public function __invoke($request, $response, $next) {
- if (!$this->authenticator->check()) {
- throw new AuthExpiredException();
- } else {
- return $next($request, $response);
- }
-
- return $response;
- }
-}
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Account\Authenticate;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Slim\Http\Body;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException;
+
+/**
+ * Middleware to catch requests that fail because they require user authentication.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AuthGuard
+{
+ /**
+ * @var Authenticator
+ */
+ protected $authenticator;
+
+ /**
+ * Constructor.
+ *
+ * @param $authenticator Authenticator The current authentication object.
+ */
+ public function __construct($authenticator) {
+ $this->authenticator = $authenticator;
+ }
+
+ /**
+ * Invoke the AuthGuard middleware, throwing an exception if there is no authenticated user in the session.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request
+ * @param \Psr\Http\Message\ResponseInterface $response PSR7 response
+ * @param callable $next Next middleware
+ *
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function __invoke($request, $response, $next) {
+ if (!$this->authenticator->check()) {
+ throw new AuthExpiredException();
+ } else {
+ return $next($request, $response);
+ }
+
+ return $response;
+ }
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Authenticator.php b/main/app/sprinkles/account/src/Authenticate/Authenticator.php
index 735a688..a4586e4 100644
--- a/main/app/sprinkles/account/src/Authenticate/Authenticator.php
+++ b/main/app/sprinkles/account/src/Authenticate/Authenticator.php
@@ -1,407 +1,407 @@
-<?php
-/**
- * UserFrosting (http://www.userfrosting.com)
- *
- * @link https://github.com/userfrosting/UserFrosting
- * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
- */
-
-namespace UserFrosting\Sprinkle\Account\Authenticate;
-
-use Birke\Rememberme\Authenticator as RememberMe;
-use Birke\Rememberme\Storage\PDOStorage as RememberMePDO;
-use Birke\Rememberme\Triplet as RememberMeTriplet;
-use Illuminate\Database\Capsule\Manager as Capsule;
-use UserFrosting\Session\Session;
-use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountDisabledException;
-use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountInvalidException;
-use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountNotVerifiedException;
-use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException;
-use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException;
-use UserFrosting\Sprinkle\Account\Authenticate\Exception\InvalidCredentialsException;
-use UserFrosting\Sprinkle\Account\Database\Models\User;
-use UserFrosting\Sprinkle\Account\Facades\Password;
-use UserFrosting\Sprinkle\Core\Util\ClassMapper;
-
-/**
- * Handles authentication tasks.
- *
- * @author Alex Weissman (https://alexanderweissman.com)
- * Partially inspired by Laravel's Authentication component: https://github.com/laravel/framework/blob/5.3/src/Illuminate/Auth/SessionGuard.php
- */
-class Authenticator
-{
- /**
- * @var ClassMapper
- */
- protected $classMapper;
-
- /**
- * @var Session
- */
- protected $session;
-
- /**
- * @var Config
- */
- protected $config;
-
- /**
- * @var Cache
- */
- protected $cache;
-
- /**
- * @var bool
- */
- protected $loggedOut = FALSE;
-
- /**
- * @var RememberMePDO
- */
- protected $rememberMeStorage;
-
- /**
- * @var RememberMe
- */
- protected $rememberMe;
-
- /**
- * @var User
- */
- protected $user;
-
- /**
- * Indicates if the user was authenticated via a rememberMe cookie.
- *
- * @var bool
- */
- protected $viaRemember = FALSE;
-
- /**
- * Create a new Authenticator object.
- *
- * @param ClassMapper $classMapper Maps generic class identifiers to specific class names.
- * @param Session $session The session wrapper object that will store the user's id.
- * @param Config $config Config object that contains authentication settings.
- * @param mixed $cache Cache service instance
- */
- public function __construct(ClassMapper $classMapper, Session $session, $config, $cache) {
- $this->classMapper = $classMapper;
- $this->session = $session;
- $this->config = $config;
- $this->cache = $cache;
-
- // Initialize RememberMe storage
- $this->rememberMeStorage = new RememberMePDO($this->config['remember_me.table']);
-
- // Get the actual PDO instance from Eloquent
- $pdo = Capsule::connection()->getPdo();
-
- $this->rememberMeStorage->setConnection($pdo);
-
- // Set up RememberMe
- $this->rememberMe = new RememberMe($this->rememberMeStorage);
- // Set cookie name
- $cookieName = $this->config['session.name'] . '-' . $this->config['remember_me.cookie.name'];
- $this->rememberMe->getCookie()->setName($cookieName);
-
- // Change cookie path
- $this->rememberMe->getCookie()->setPath($this->config['remember_me.session.path']);
-
- // Set expire time, if specified
- if ($this->config->has('remember_me.expire_time') && ($this->config->has('remember_me.expire_time') != NULL)) {
- $this->rememberMe->getCookie()->setExpireTime($this->config['remember_me.expire_time']);
- }
-
- $this->user = NULL;
-
- $this->viaRemember = FALSE;
- }
-
- /**
- * Attempts to authenticate a user based on a supplied identity and password.
- *
- * If successful, the user's id is stored in session.
- */
- public function attempt($identityColumn, $identityValue, $password, $rememberMe = FALSE) {
- // Try to load the user, using the specified conditions
- $user = $this->classMapper->staticMethod('user', 'where', $identityColumn, $identityValue)->first();
-
- if (!$user) {
- throw new InvalidCredentialsException();
- }
-
- // Check that the user has a password set (so, rule out newly created accounts without a password)
- if (!$user->password) {
- throw new InvalidCredentialsException();
- }
-
- // Check that the user's account is enabled
- if ($user->flag_enabled == 0) {
- throw new AccountDisabledException();
- }
-
- // Check that the user's account is verified (if verification is required)
- if ($this->config['site.registration.require_email_verification'] && $user->flag_verified == 0) {
- throw new AccountNotVerifiedException();
- }
-
- // Here is my password. May I please assume the identify of this user now?
- if (Password::verify($password, $user->password)) {
- $this->login($user, $rememberMe);
- return $user;
- } else {
- // We know the password is at fault here (as opposed to the identity), but lets not give away the combination in case of someone bruteforcing
- throw new InvalidCredentialsException();
- }
- }
-
- /**
- * Determine if the current user is authenticated.
- *
- * @return bool
- */
- public function check() {
- return !is_null($this->user());
- }
-
- /**
- * Determine if the current user is a guest (unauthenticated).
- *
- * @return bool
- */
- public function guest() {
- return !$this->check();
- }
-
- /**
- * Process an account login request.
- *
- * This method logs in the specified user, allowing the client to assume the user's identity for the duration of the session.
- * @param User $user The user to log in.
- * @param bool $rememberMe Set to true to make this a "persistent session", i.e. one that will re-login even after the session expires.
- * @odo Figure out a way to update the currentUser service to reflect the logged-in user *immediately* in the service provider.
- * As it stands, the currentUser service will still reflect a "guest user" for the remainder of the request.
- */
- public function login($user, $rememberMe = FALSE) {
- $oldId = session_id();
- $this->session->regenerateId(TRUE);
-
- // Since regenerateId deletes the old session, we'll do the same in cache
- $this->flushSessionCache($oldId);
-
- // If the user wants to be remembered, create Rememberme cookie
- if ($rememberMe) {
- $this->rememberMe->createCookie($user->id);
- } else {
- $this->rememberMe->clearCookie();
- }
-
- // Assume identity
- $key = $this->config['session.keys.current_user_id'];
- $this->session[$key] = $user->id;
-
- // Set auth mode
- $this->viaRemember = FALSE;
-
- // User login actions
- $user->onLogin();
- }
-
- /**
- * Processes an account logout request.
- *
- * Logs the currently authenticated user out, destroying the PHP session and clearing the persistent session.
- * This can optionally remove persistent sessions across all browsers/devices, since there can be a "RememberMe" cookie
- * and corresponding database entries in multiple browsers/devices. See http://jaspan.com/improved_persistent_login_cookie_best_practice.
- *
- * @param bool $complete If set to true, will ensure that the user is logged out from *all* browsers on all devices.
- */
- public function logout($complete = FALSE) {
- $currentUserId = $this->session->get($this->config['session.keys.current_user_id']);
-
- // This removes all of the user's persistent logins from the database
- if ($complete) {
- $this->storage->cleanAllTriplets($currentUserId);
- }
-
- // Clear the rememberMe cookie
- $this->rememberMe->clearCookie();
-
- // User logout actions
- if ($currentUserId) {
- $currentUser = $this->classMapper->staticMethod('user', 'find', $currentUserId);
- if ($currentUser) {
- $currentUser->onLogout();
- }
- }
-
- $this->user = NULL;
- $this->loggedOut = TRUE;
-
- $oldId = session_id();
-
- // Completely destroy the session
- $this->session->destroy();
-
- // Since regenerateId deletes the old session, we'll do the same in cache
- $this->flushSessionCache($oldId);
-
- // Restart the session service
- $this->session->start();
- }
-
- /**
- * Try to get the currently authenticated user, returning a guest user if none was found.
- *
- * Tries to re-establish a session for "remember-me" users who have been logged out due to an expired session.
- * @return User|null
- * @throws AuthExpiredException
- * @throws AuthCompromisedException
- * @throws AccountInvalidException
- * @throws AccountDisabledException
- */
- public function user() {
- $user = NULL;
-
- if (!$this->loggedOut) {
-
- // Return any cached user
- if (!is_null($this->user)) {
- return $this->user;
- }
-
- // If this throws a PDOException we catch it and return null than allowing the exception to propagate.
- // This is because the error handler relies on Twig, which relies on a Twig Extension, which relies on the global current_user variable.
- // So, we really don't want this method to throw any database exceptions.
- try {
- // Now, check to see if we have a user in session
- $user = $this->loginSessionUser();
-
- // If no user was found in the session, try to login via RememberMe cookie
- if (!$user) {
- $user = $this->loginRememberedUser();
- }
- } catch (\PDOException $e) {
- $user = NULL;
- }
- }
-
- return $this->user = $user;
- }
-
- /**
- * Determine whether the current user was authenticated using a remember me cookie.
- *
- * This function is useful when users are performing sensitive operations, and you may want to force them to re-authenticate.
- * @return bool
- */
- public function viaRemember() {
- return $this->viaRemember;
- }
-
- /**
- * Attempt to log in the client from their rememberMe token (in their cookie).
- *
- * @return User|bool If successful, the User object of the remembered user. Otherwise, return false.
- * @throws AuthCompromisedException The client attempted to log in with an invalid rememberMe token.
- */
- protected function loginRememberedUser() {
- /** @var \Birke\Rememberme\LoginResult $loginResult */
- $loginResult = $this->rememberMe->login();
-
- if ($loginResult->isSuccess()) {
- // Update in session
- $this->session[$this->config['session.keys.current_user_id']] = $loginResult->getCredential();
- // There is a chance that an attacker has stolen the login token,
- // so we store the fact that the user was logged in via RememberMe (instead of login form)
- $this->viaRemember = TRUE;
- } else {
- // If $rememberMe->login() was not successful, check if the token was invalid as well. This means the cookie was stolen.
- if ($loginResult->hasPossibleManipulation()) {
- throw new AuthCompromisedException();
- }
- }
-
- return $this->validateUserAccount($loginResult->getCredential());
- }
-
- /**
- * Attempt to log in the client from the session.
- *
- * @return User|null If successful, the User object of the user in session. Otherwise, return null.
- * @throws AuthExpiredException The client attempted to use an expired rememberMe token.
- */
- protected function loginSessionUser() {
- $userId = $this->session->get($this->config['session.keys.current_user_id']);
-
- // If a user_id was found in the session, check any rememberMe cookie that was submitted.
- // If they submitted an expired rememberMe cookie, then we need to log them out.
- if ($userId) {
- if (!$this->validateRememberMeCookie()) {
- $this->logout();
- throw new AuthExpiredException();
- }
- }
-
- return $this->validateUserAccount($userId);
- }
-
- /**
- * Determine if the cookie contains a valid rememberMe token.
- *
- * @return bool
- */
- protected function validateRememberMeCookie() {
- $cookieValue = $this->rememberMe->getCookie()->getValue();
- if (!$cookieValue) {
- return TRUE;
- }
- $triplet = RememberMeTriplet::fromString($cookieValue);
- if (!$triplet->isValid()) {
- return FALSE;
- }
-
- return TRUE;
- }
-
- /**
- * Tries to load the specified user by id from the database.
- *
- * Checks that the account is valid and enabled, throwing an exception if not.
- * @param int $userId
- * @return User|null
- * @throws AccountInvalidException
- * @throws AccountDisabledException
- */
- protected function validateUserAccount($userId) {
- if ($userId) {
- $user = $this->classMapper->staticMethod('user', 'find', $userId);
-
- // If the user doesn't exist any more, throw an exception.
- if (!$user) {
- throw new AccountInvalidException();
- }
-
- // If the user has been disabled since their last request, throw an exception.
- if (!$user->flag_enabled) {
- throw new AccountDisabledException();
- }
-
- return $user;
- } else {
- return NULL;
- }
- }
-
- /**
- * Flush the cache associated with a session id
- *
- * @param string $id The session id
- * @return bool
- */
- public function flushSessionCache($id) {
- return $this->cache->tags('_s' . $id)->flush();
- }
-}
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Account\Authenticate;
+
+use Birke\Rememberme\Authenticator as RememberMe;
+use Birke\Rememberme\Storage\PDOStorage as RememberMePDO;
+use Birke\Rememberme\Triplet as RememberMeTriplet;
+use Illuminate\Database\Capsule\Manager as Capsule;
+use UserFrosting\Session\Session;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountDisabledException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountInvalidException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AccountNotVerifiedException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthCompromisedException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\AuthExpiredException;
+use UserFrosting\Sprinkle\Account\Authenticate\Exception\InvalidCredentialsException;
+use UserFrosting\Sprinkle\Account\Database\Models\User;
+use UserFrosting\Sprinkle\Account\Facades\Password;
+use UserFrosting\Sprinkle\Core\Util\ClassMapper;
+
+/**
+ * Handles authentication tasks.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ * Partially inspired by Laravel's Authentication component: https://github.com/laravel/framework/blob/5.3/src/Illuminate/Auth/SessionGuard.php
+ */
+class Authenticator
+{
+ /**
+ * @var ClassMapper
+ */
+ protected $classMapper;
+
+ /**
+ * @var Session
+ */
+ protected $session;
+
+ /**
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * @var Cache
+ */
+ protected $cache;
+
+ /**
+ * @var bool
+ */
+ protected $loggedOut = FALSE;
+
+ /**
+ * @var RememberMePDO
+ */
+ protected $rememberMeStorage;
+
+ /**
+ * @var RememberMe
+ */
+ protected $rememberMe;
+
+ /**
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * Indicates if the user was authenticated via a rememberMe cookie.
+ *
+ * @var bool
+ */
+ protected $viaRemember = FALSE;
+
+ /**
+ * Create a new Authenticator object.
+ *
+ * @param ClassMapper $classMapper Maps generic class identifiers to specific class names.
+ * @param Session $session The session wrapper object that will store the user's id.
+ * @param Config $config Config object that contains authentication settings.
+ * @param mixed $cache Cache service instance
+ */
+ public function __construct(ClassMapper $classMapper, Session $session, $config, $cache) {
+ $this->classMapper = $classMapper;
+ $this->session = $session;
+ $this->config = $config;
+ $this->cache = $cache;
+
+ // Initialize RememberMe storage
+ $this->rememberMeStorage = new RememberMePDO($this->config['remember_me.table']);
+
+ // Get the actual PDO instance from Eloquent
+ $pdo = Capsule::connection()->getPdo();
+
+ $this->rememberMeStorage->setConnection($pdo);
+
+ // Set up RememberMe
+ $this->rememberMe = new RememberMe($this->rememberMeStorage);
+ // Set cookie name
+ $cookieName = $this->config['session.name'] . '-' . $this->config['remember_me.cookie.name'];
+ $this->rememberMe->getCookie()->setName($cookieName);
+
+ // Change cookie path
+ $this->rememberMe->getCookie()->setPath($this->config['remember_me.session.path']);
+
+ // Set expire time, if specified
+ if ($this->config->has('remember_me.expire_time') && ($this->config->has('remember_me.expire_time') != NULL)) {
+ $this->rememberMe->getCookie()->setExpireTime($this->config['remember_me.expire_time']);
+ }
+
+ $this->user = NULL;
+
+ $this->viaRemember = FALSE;
+ }
+
+ /**
+ * Attempts to authenticate a user based on a supplied identity and password.
+ *
+ * If successful, the user's id is stored in session.
+ */
+ public function attempt($identityColumn, $identityValue, $password, $rememberMe = FALSE) {
+ // Try to load the user, using the specified conditions
+ $user = $this->classMapper->staticMethod('user', 'where', $identityColumn, $identityValue)->first();
+
+ if (!$user) {
+ throw new InvalidCredentialsException();
+ }
+
+ // Check that the user has a password set (so, rule out newly created accounts without a password)
+ if (!$user->password) {
+ throw new InvalidCredentialsException();
+ }
+
+ // Check that the user's account is enabled
+ if ($user->flag_enabled == 0) {
+ throw new AccountDisabledException();
+ }
+
+ // Check that the user's account is verified (if verification is required)
+ if ($this->config['site.registration.require_email_verification'] && $user->flag_verified == 0) {
+ throw new AccountNotVerifiedException();
+ }
+
+ // Here is my password. May I please assume the identify of this user now?
+ if (Password::verify($password, $user->password)) {
+ $this->login($user, $rememberMe);
+ return $user;
+ } else {
+ // We know the password is at fault here (as opposed to the identity), but lets not give away the combination in case of someone bruteforcing
+ throw new InvalidCredentialsException();
+ }
+ }
+
+ /**
+ * Determine if the current user is authenticated.
+ *
+ * @return bool
+ */
+ public function check() {
+ return !is_null($this->user());
+ }
+
+ /**
+ * Determine if the current user is a guest (unauthenticated).
+ *
+ * @return bool
+ */
+ public function guest() {
+ return !$this->check();
+ }
+
+ /**
+ * Process an account login request.
+ *
+ * This method logs in the specified user, allowing the client to assume the user's identity for the duration of the session.
+ * @param User $user The user to log in.
+ * @param bool $rememberMe Set to true to make this a "persistent session", i.e. one that will re-login even after the session expires.
+ * @odo Figure out a way to update the currentUser service to reflect the logged-in user *immediately* in the service provider.
+ * As it stands, the currentUser service will still reflect a "guest user" for the remainder of the request.
+ */
+ public function login($user, $rememberMe = FALSE) {
+ $oldId = session_id();
+ $this->session->regenerateId(TRUE);
+
+ // Since regenerateId deletes the old session, we'll do the same in cache
+ $this->flushSessionCache($oldId);
+
+ // If the user wants to be remembered, create Rememberme cookie
+ if ($rememberMe) {
+ $this->rememberMe->createCookie($user->id);
+ } else {
+ $this->rememberMe->clearCookie();
+ }
+
+ // Assume identity
+ $key = $this->config['session.keys.current_user_id'];
+ $this->session[$key] = $user->id;
+
+ // Set auth mode
+ $this->viaRemember = FALSE;
+
+ // User login actions
+ $user->onLogin();
+ }
+
+ /**
+ * Processes an account logout request.
+ *
+ * Logs the currently authenticated user out, destroying the PHP session and clearing the persistent session.
+ * This can optionally remove persistent sessions across all browsers/devices, since there can be a "RememberMe" cookie
+ * and corresponding database entries in multiple browsers/devices. See http://jaspan.com/improved_persistent_login_cookie_best_practice.
+ *
+ * @param bool $complete If set to true, will ensure that the user is logged out from *all* browsers on all devices.
+ */
+ public function logout($complete = FALSE) {
+ $currentUserId = $this->session->get($this->config['session.keys.current_user_id']);
+
+ // This removes all of the user's persistent logins from the database
+ if ($complete) {
+ $this->storage->cleanAllTriplets($currentUserId);
+ }
+
+ // Clear the rememberMe cookie
+ $this->rememberMe->clearCookie();
+
+ // User logout actions
+ if ($currentUserId) {
+ $currentUser = $this->classMapper->staticMethod('user', 'find', $currentUserId);
+ if ($currentUser) {
+ $currentUser->onLogout();
+ }
+ }
+
+ $this->user = NULL;
+ $this->loggedOut = TRUE;
+
+ $oldId = session_id();
+
+ // Completely destroy the session
+ $this->session->destroy();
+
+ // Since regenerateId deletes the old session, we'll do the same in cache
+ $this->flushSessionCache($oldId);
+
+ // Restart the session service
+ $this->session->start();
+ }
+
+ /**
+ * Try to get the currently authenticated user, returning a guest user if none was found.
+ *
+ * Tries to re-establish a session for "remember-me" users who have been logged out due to an expired session.
+ * @return User|null
+ * @throws AuthExpiredException
+ * @throws AuthCompromisedException
+ * @throws AccountInvalidException
+ * @throws AccountDisabledException
+ */
+ public function user() {
+ $user = NULL;
+
+ if (!$this->loggedOut) {
+
+ // Return any cached user
+ if (!is_null($this->user)) {
+ return $this->user;
+ }
+
+ // If this throws a PDOException we catch it and return null than allowing the exception to propagate.
+ // This is because the error handler relies on Twig, which relies on a Twig Extension, which relies on the global current_user variable.
+ // So, we really don't want this method to throw any database exceptions.
+ try {
+ // Now, check to see if we have a user in session
+ $user = $this->loginSessionUser();
+
+ // If no user was found in the session, try to login via RememberMe cookie
+ if (!$user) {
+ $user = $this->loginRememberedUser();
+ }
+ } catch (\PDOException $e) {
+ $user = NULL;
+ }
+ }
+
+ return $this->user = $user;
+ }
+
+ /**
+ * Determine whether the current user was authenticated using a remember me cookie.
+ *
+ * This function is useful when users are performing sensitive operations, and you may want to force them to re-authenticate.
+ * @return bool
+ */
+ public function viaRemember() {
+ return $this->viaRemember;
+ }
+
+ /**
+ * Attempt to log in the client from their rememberMe token (in their cookie).
+ *
+ * @return User|bool If successful, the User object of the remembered user. Otherwise, return false.
+ * @throws AuthCompromisedException The client attempted to log in with an invalid rememberMe token.
+ */
+ protected function loginRememberedUser() {
+ /** @var \Birke\Rememberme\LoginResult $loginResult */
+ $loginResult = $this->rememberMe->login();
+
+ if ($loginResult->isSuccess()) {
+ // Update in session
+ $this->session[$this->config['session.keys.current_user_id']] = $loginResult->getCredential();
+ // There is a chance that an attacker has stolen the login token,
+ // so we store the fact that the user was logged in via RememberMe (instead of login form)
+ $this->viaRemember = TRUE;
+ } else {
+ // If $rememberMe->login() was not successful, check if the token was invalid as well. This means the cookie was stolen.
+ if ($loginResult->hasPossibleManipulation()) {
+ throw new AuthCompromisedException();
+ }
+ }
+
+ return $this->validateUserAccount($loginResult->getCredential());
+ }
+
+ /**
+ * Attempt to log in the client from the session.
+ *
+ * @return User|null If successful, the User object of the user in session. Otherwise, return null.
+ * @throws AuthExpiredException The client attempted to use an expired rememberMe token.
+ */
+ protected function loginSessionUser() {
+ $userId = $this->session->get($this->config['session.keys.current_user_id']);
+
+ // If a user_id was found in the session, check any rememberMe cookie that was submitted.
+ // If they submitted an expired rememberMe cookie, then we need to log them out.
+ if ($userId) {
+ if (!$this->validateRememberMeCookie()) {
+ $this->logout();
+ throw new AuthExpiredException();
+ }
+ }
+
+ return $this->validateUserAccount($userId);
+ }
+
+ /**
+ * Determine if the cookie contains a valid rememberMe token.
+ *
+ * @return bool
+ */
+ protected function validateRememberMeCookie() {
+ $cookieValue = $this->rememberMe->getCookie()->getValue();
+ if (!$cookieValue) {
+ return TRUE;
+ }
+ $triplet = RememberMeTriplet::fromString($cookieValue);
+ if (!$triplet->isValid()) {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Tries to load the specified user by id from the database.
+ *
+ * Checks that the account is valid and enabled, throwing an exception if not.
+ * @param int $userId
+ * @return User|null
+ * @throws AccountInvalidException
+ * @throws AccountDisabledException
+ */
+ protected function validateUserAccount($userId) {
+ if ($userId) {
+ $user = $this->classMapper->staticMethod('user', 'find', $userId);
+
+ // If the user doesn't exist any more, throw an exception.
+ if (!$user) {
+ throw new AccountInvalidException();
+ }
+
+ // If the user has been disabled since their last request, throw an exception.
+ if (!$user->flag_enabled) {
+ throw new AccountDisabledException();
+ }
+
+ return $user;
+ } else {
+ return NULL;
+ }
+ }
+
+ /**
+ * Flush the cache associated with a session id
+ *
+ * @param string $id The session id
+ * @return bool
+ */
+ public function flushSessionCache($id) {
+ return $this->cache->tags('_s' . $id)->flush();
+ }
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php
index 3ad4c59..314fcc3 100644
--- a/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AccountDisabledException.php
@@ -1,22 +1,22 @@
-<?php
-/**
- * UserFrosting (http://www.userfrosting.com)
- *
- * @link https://github.com/userfrosting/UserFrosting
- * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
- */
-
-namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
-
-use UserFrosting\Support\Exception\HttpException;
-
-/**
- * Disabled account exception. Used when an account has been disabled.
- *
- * @author Alex Weissman (https://alexanderweissman.com)
- */
-class AccountDisabledException extends HttpException
-{
- protected $defaultMessage = 'ACCOUNT.DISABLED';
- protected $httpErrorCode = 403;
-}
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Disabled account exception. Used when an account has been disabled.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AccountDisabledException extends HttpException
+{
+ protected $defaultMessage = 'ACCOUNT.DISABLED';
+ protected $httpErrorCode = 403;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php
index fb06fae..bc42b62 100644
--- a/main/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AccountInvalidException.php
@@ -1,22 +1,22 @@
-<?php
-/**
- * UserFrosting (http://www.userfrosting.com)
- *
- * @link https://github.com/userfrosting/UserFrosting
- * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
- */
-
-namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
-
-use UserFrosting\Support\Exception\HttpException;
-
-/**
- * Invalid account exception. Used when an account has been removed during an active session.
- *
- * @author Alex Weissman (https://alexanderweissman.com)
- */
-class AccountInvalidException extends HttpException
-{
- protected $defaultMessage = 'ACCOUNT.INVALID';
- protected $httpErrorCode = 403;
-}
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Invalid account exception. Used when an account has been removed during an active session.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AccountInvalidException extends HttpException
+{
+ protected $defaultMessage = 'ACCOUNT.INVALID';
+ protected $httpErrorCode = 403;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php
index 2a721bb..1548201 100644
--- a/main/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AccountNotVerifiedException.php
@@ -1,22 +1,22 @@
-<?php
-/**
- * UserFrosting (http://www.userfrosting.com)
- *
- * @link https://github.com/userfrosting/UserFrosting
- * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
- */
-
-namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
-
-use UserFrosting\Support\Exception\HttpException;
-
-/**
- * Unverified account exception. Used when an account is required to complete email verification, but hasn't done so yet.
- *
- * @author Alex Weissman (https://alexanderweissman.com)
- */
-class AccountNotVerifiedException extends HttpException
-{
- protected $defaultMessage = 'ACCOUNT.UNVERIFIED';
- protected $httpErrorCode = 403;
-}
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Unverified account exception. Used when an account is required to complete email verification, but hasn't done so yet.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AccountNotVerifiedException extends HttpException
+{
+ protected $defaultMessage = 'ACCOUNT.UNVERIFIED';
+ protected $httpErrorCode = 403;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php
index 52fd528..dd169bd 100644
--- a/main/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AuthCompromisedException.php
@@ -1,21 +1,21 @@
-<?php
-/**
- * UserFrosting (http://www.userfrosting.com)
- *
- * @link https://github.com/userfrosting/UserFrosting
- * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
- */
-
-namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
-
-use UserFrosting\Support\Exception\ForbiddenException;
-
-/**
- * Compromised authentication exception. Used when we suspect theft of the rememberMe cookie.
- *
- * @author Alex Weissman (https://alexanderweissman.com)
- */
-class AuthCompromisedException extends ForbiddenException
-{
- protected $defaultMessage = 'ACCOUNT.SESSION_COMPROMISED';
-}
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\ForbiddenException;
+
+/**
+ * Compromised authentication exception. Used when we suspect theft of the rememberMe cookie.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AuthCompromisedException extends ForbiddenException
+{
+ protected $defaultMessage = 'ACCOUNT.SESSION_COMPROMISED';
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php b/main/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php
index ab7cbdb..2345118 100644
--- a/main/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/AuthExpiredException.php
@@ -1,22 +1,22 @@
-<?php
-/**
- * UserFrosting (http://www.userfrosting.com)
- *
- * @link https://github.com/userfrosting/UserFrosting
- * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
- */
-
-namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
-
-use UserFrosting\Support\Exception\HttpException;
-
-/**
- * Expired authentication exception. Used when the user needs to authenticate/reauthenticate.
- *
- * @author Alex Weissman (https://alexanderweissman.com)
- */
-class AuthExpiredException extends HttpException
-{
- protected $defaultMessage = 'ACCOUNT.SESSION_EXPIRED';
- protected $httpErrorCode = 401;
-}
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Expired authentication exception. Used when the user needs to authenticate/reauthenticate.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class AuthExpiredException extends HttpException
+{
+ protected $defaultMessage = 'ACCOUNT.SESSION_EXPIRED';
+ protected $httpErrorCode = 401;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php b/main/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php
index 78ea3de..8f73403 100644
--- a/main/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php
+++ b/main/app/sprinkles/account/src/Authenticate/Exception/InvalidCredentialsException.php
@@ -1,22 +1,22 @@
-<?php
-/**
- * UserFrosting (http://www.userfrosting.com)
- *
- * @link https://github.com/userfrosting/UserFrosting
- * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
- */
-
-namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
-
-use UserFrosting\Support\Exception\HttpException;
-
-/**
- * Invalid credentials exception. Used when an account fails authentication for some reason.
- *
- * @author Alex Weissman (https://alexanderweissman.com)
- */
-class InvalidCredentialsException extends HttpException
-{
- protected $defaultMessage = 'USER_OR_PASS_INVALID';
- protected $httpErrorCode = 403;
-}
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Account\Authenticate\Exception;
+
+use UserFrosting\Support\Exception\HttpException;
+
+/**
+ * Invalid credentials exception. Used when an account fails authentication for some reason.
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class InvalidCredentialsException extends HttpException
+{
+ protected $defaultMessage = 'USER_OR_PASS_INVALID';
+ protected $httpErrorCode = 403;
+}
diff --git a/main/app/sprinkles/account/src/Authenticate/Hasher.php b/main/app/sprinkles/account/src/Authenticate/Hasher.php
index 5de939f..7ec832e 100644
--- a/main/app/sprinkles/account/src/Authenticate/Hasher.php
+++ b/main/app/sprinkles/account/src/Authenticate/Hasher.php
@@ -1,105 +1,105 @@
-<?php
-/**
- * UserFrosting (http://www.userfrosting.com)
- *
- * @link https://github.com/userfrosting/UserFrosting
- * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
- */
-
-namespace UserFrosting\Sprinkle\Account\Authenticate;
-
-/**
- * Password hashing and validation class
- *
- * @author Alex Weissman (https://alexanderweissman.com)
- */
-class Hasher
-{
- /**
- * Default crypt cost factor.
- *
- * @var int
- */
- protected $defaultRounds = 10;
-
- /**
- * Returns the hashing type for a specified password hash.
- *
- * Automatically detects the hash type: "sha1" (for UserCake legacy accounts), "legacy" (for 0.1.x accounts), and "modern" (used for new accounts).
- * @param string $password the hashed password.
- * @return string "sha1"|"legacy"|"modern".
- */
- public function getHashType($password) {
- // If the password in the db is 65 characters long, we have an sha1-hashed password.
- if (strlen($password) == 65) {
- return 'sha1';
- } else if (strlen($password) == 82) {
- return 'legacy';
- }
-
- return 'modern';
- }
-
- /**
- * Hashes a plaintext password using bcrypt.
- *
- * @param string $password the plaintext password.
- * @param array $options
- * @return string the hashed password.
- * @throws HashFailedException
- */
- public function hash($password, array $options = []) {
- $hash = password_hash($password, PASSWORD_BCRYPT, [
- 'cost' => $this->cost($options),
- ]);
-
- if (!$hash) {
- throw new HashFailedException();
- }
-
- return $hash;
- }
-
- /**
- * Verify a plaintext password against the user's hashed password.
- *
- * @param string $password The plaintext password to verify.
- * @param string $hash The hash to compare against.
- * @param array $options
- * @return boolean True if the password matches, false otherwise.
- */
- public function verify($password, $hash, array $options = []) {
- $hashType = $this->getHashType($hash);
-
- if ($hashType == 'sha1') {
- // Legacy UserCake passwords
- $salt = substr($hash, 0, 25); // Extract the salt from the hash
- $inputHash = $salt . sha1($salt . $password);
-
- return (hash_equals($inputHash, $hash) === TRUE);
-
- } else if ($hashType == 'legacy') {
- // Homegrown implementation (assuming that current install has been using a cost parameter of 12)
- // Used for manual implementation of bcrypt.
- // Note that this legacy hashing put the salt at the _end_ for some reason.
- $salt = substr($hash, 60);
- $inputHash = crypt($password, '$2y$12$' . $salt);
- $correctHash = substr($hash, 0, 60);
-
- return (hash_equals($inputHash, $correctHash) === TRUE);
- }
-
- // Modern implementation
- return password_verify($password, $hash);
- }
-
- /**
- * Extract the cost value from the options array.
- *
- * @param array $options
- * @return int
- */
- protected function cost(array $options = []) {
- return isset($options['rounds']) ? $options['rounds'] : $this->defaultRounds;
- }
-}
+<?php
+/**
+ * UserFrosting (http://www.userfrosting.com)
+ *
+ * @link https://github.com/userfrosting/UserFrosting
+ * @license https://github.com/userfrosting/UserFrosting/blob/master/licenses/UserFrosting.md (MIT License)
+ */
+
+namespace UserFrosting\Sprinkle\Account\Authenticate;
+
+/**
+ * Password hashing and validation class
+ *
+ * @author Alex Weissman (https://alexanderweissman.com)
+ */
+class Hasher
+{
+ /**
+ * Default crypt cost factor.
+ *
+ * @var int
+ */
+ protected $defaultRounds = 10;
+
+ /**
+ * Returns the hashing type for a specified password hash.
+ *
+ * Automatically detects the hash type: "sha1" (for UserCake legacy accounts), "legacy" (for 0.1.x accounts), and "modern" (used for new accounts).
+ * @param string $password the hashed password.
+ * @return string "sha1"|"legacy"|"modern".
+ */
+ public function getHashType($password) {
+ // If the password in the db is 65 characters long, we have an sha1-hashed password.
+ if (strlen($password) == 65) {
+ return 'sha1';
+ } else if (strlen($password) == 82) {
+ return 'legacy';
+ }
+
+ return 'modern';
+ }
+
+ /**
+ * Hashes a plaintext password using bcrypt.
+ *
+ * @param string $password the plaintext password.
+ * @param array $options
+ * @return string the hashed password.
+ * @throws HashFailedException
+ */
+ public function hash($password, array $options = []) {
+ $hash = password_hash($password, PASSWORD_BCRYPT, [
+ 'cost' => $this->cost($options),
+ ]);
+
+ if (!$hash) {
+ throw new HashFailedException();
+ }
+
+ return $hash;
+ }
+
+ /**
+ * Verify a plaintext password against the user's hashed password.
+ *
+ * @param string $password The plaintext password to verify.
+ * @param string $hash The hash to compare against.
+ * @param array $options
+ * @return boolean True if the password matches, false otherwise.
+ */
+ public function verify($password, $hash, array $options = []) {
+ $hashType = $this->getHashType($hash);
+
+ if ($hashType == 'sha1') {
+ // Legacy UserCake passwords
+ $salt = substr($hash, 0, 25); // Extract the salt from the hash
+ $inputHash = $salt . sha1($salt . $password);
+
+ return (hash_equals($inputHash, $hash) === TRUE);
+
+ } else if ($hashType == 'legacy') {
+ // Homegrown implementation (assuming that current install has been using a cost parameter of 12)
+ // Used for manual implementation of bcrypt.
+ // Note that this legacy hashing put the salt at the _end_ for some reason.
+ $salt = substr($hash, 60);
+ $inputHash = crypt($password, '$2y$12$' . $salt);
+ $correctHash = substr($hash, 0, 60);
+
+ return (hash_equals($inputHash, $correctHash) === TRUE);
+ }
+
+ // Modern implementation
+ return password_verify($password, $hash);
+ }
+
+ /**
+ * Extract the cost value from the options array.
+ *
+ * @param array $options
+ * @return int
+ */
+ protected function cost(array $options = []) {
+ return isset($options['rounds']) ? $options['rounds'] : $this->defaultRounds;
+ }
+}