1
0
mirror of https://github.com/delight-im/PHP-Auth.git synced 2025-08-07 00:26:28 +02:00

9 Commits

5 changed files with 268 additions and 107 deletions

View File

@@ -79,6 +79,7 @@ Migrating from an earlier version of this project? See our [upgrade guide](Migra
* [Assigning roles to users](#assigning-roles-to-users)
* [Taking roles away from users](#taking-roles-away-from-users)
* [Checking roles](#checking-roles-1)
* [Impersonating users (logging in as user)](#impersonating-users-logging-in-as-user)
* [Cookies](#cookies)
* [Renaming the librarys cookies](#renaming-the-librarys-cookies)
* [Defining the domain scope for cookies](#defining-the-domain-scope-for-cookies)
@@ -898,6 +899,47 @@ catch (\Delight\Auth\UnknownIdException $e) {
}
```
#### Impersonating users (logging in as user)
```php
try {
$auth->admin()->logInAsUserById($_POST['id']);
}
catch (\Delight\Auth\UnknownIdException $e) {
// unknown ID
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
// email address not verified
}
// or
try {
$auth->admin()->logInAsUserByEmail($_POST['email']);
}
catch (\Delight\Auth\InvalidEmailException $e) {
// unknown email address
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
// email address not verified
}
// or
try {
$auth->admin()->logInAsUserByUsername($_POST['username']);
}
catch (\Delight\Auth\UnknownUsernameException $e) {
// unknown username
}
catch (\Delight\Auth\AmbiguousUsernameException $e) {
// ambiguous username
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
// email address not verified
}
```
### Cookies
This library uses two cookies to keep state on the client: The first, whose name you can retrieve using

View File

@@ -286,6 +286,60 @@ final class Administration extends UserManager {
return ($rolesBitmask & $role) === $role;
}
/**
* Signs in as the user with the specified ID
*
* @param int $id the ID of the user to sign in as
* @throws UnknownIdException if no user with the specified ID has been found
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logInAsUserById($id) {
$numberOfMatchedUsers = $this->logInAsUserByColumnValue('id', (int) $id);
if ($numberOfMatchedUsers === 0) {
throw new UnknownIdException();
}
}
/**
* Signs in as the user with the specified email address
*
* @param string $email the email address of the user to sign in as
* @throws InvalidEmailException if no user with the specified email address has been found
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logInAsUserByEmail($email) {
$email = self::validateEmailAddress($email);
$numberOfMatchedUsers = $this->logInAsUserByColumnValue('email', $email);
if ($numberOfMatchedUsers === 0) {
throw new InvalidEmailException();
}
}
/**
* Signs in as the user with the specified display name
*
* @param string $username the display name of the user to sign in as
* @throws UnknownUsernameException if no user with the specified username has been found
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function logInAsUserByUsername($username) {
$numberOfMatchedUsers = $this->logInAsUserByColumnValue('username', \trim($username));
if ($numberOfMatchedUsers === 0) {
throw new UnknownUsernameException();
}
elseif ($numberOfMatchedUsers > 1) {
throw new AmbiguousUsernameException();
}
}
/**
* Deletes all existing users where the column with the specified name has the given value
*
@@ -404,4 +458,42 @@ final class Administration extends UserManager {
);
}
/**
* Signs in as the user for which the column with the specified name has the given value
*
* You must never pass untrusted input to the parameter that takes the column name
*
* @param string $columnName the name of the column to filter by
* @param mixed $columnValue the value to look for in the selected column
* @return int the number of matched users (where only a value of one means that the login may have been successful)
* @throws EmailNotVerifiedException if the user has not verified their email address via a confirmation method yet
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function logInAsUserByColumnValue($columnName, $columnValue) {
try {
$users = $this->db->select(
'SELECT verified, id, email, username, status, roles_mask FROM ' . $this->dbTablePrefix . 'users WHERE ' . $columnName . ' = ? LIMIT 2 OFFSET 0',
[ $columnValue ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
$numberOfMatchingUsers = \count($users);
if ($numberOfMatchingUsers === 1) {
$user = $users[0];
if ((int) $user['verified'] === 1) {
$this->onLoginSuccessful($user['id'], $user['email'], $user['username'], $user['status'], $user['roles_mask'], false);
}
else {
throw new EmailNotVerifiedException();
}
}
return $numberOfMatchingUsers;
}
}

View File

@@ -21,13 +21,6 @@ require_once __DIR__ . '/Exceptions.php';
/** Component that provides all features and utilities for secure authentication of individual users */
final class Auth extends UserManager {
const SESSION_FIELD_LOGGED_IN = 'auth_logged_in';
const SESSION_FIELD_USER_ID = 'auth_user_id';
const SESSION_FIELD_EMAIL = 'auth_email';
const SESSION_FIELD_USERNAME = 'auth_username';
const SESSION_FIELD_STATUS = 'auth_status';
const SESSION_FIELD_ROLES = 'auth_roles';
const SESSION_FIELD_REMEMBERED = 'auth_remembered';
const COOKIE_PREFIXES = [ Cookie::PREFIX_SECURE, Cookie::PREFIX_HOST ];
const COOKIE_CONTENT_SEPARATOR = '~';
@@ -458,18 +451,8 @@ final class Auth extends UserManager {
}
}
/**
* Called when the user has successfully logged in (via standard login or "remember me")
*
* @param int $userId the ID of the user who has just logged in
* @param string $email the email address of the user who has just logged in
* @param string $username the username (if any)
* @param int $status the status as one of the constants from the {@see Status} class
* @param int $roles the bitmask containing the roles of the user
* @param bool $remembered whether the user was remembered ("remember me") or logged in actively
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function onLoginSuccessful($userId, $email, $username, $status, $roles, $remembered) {
protected function onLoginSuccessful($userId, $email, $username, $status, $roles, $remembered) {
// update the timestamp of the user's last login
try {
$this->db->update(
$this->dbTablePrefix . 'users',
@@ -481,17 +464,7 @@ final class Auth extends UserManager {
throw new DatabaseError();
}
// re-generate the session ID to prevent session fixation attacks (requests a cookie to be written on the client)
Session::regenerate(true);
// save the user data in the session
$this->setLoggedIn(true);
$this->setUserId($userId);
$this->setEmail($email);
$this->setUsername($username);
$this->setStatus($status);
$this->setRoles($roles);
$this->setRemembered($remembered);
parent::onLoginSuccessful($userId, $email, $username, $status, $roles, $remembered);
}
/**
@@ -591,7 +564,7 @@ final class Auth extends UserManager {
// if the user has just confirmed an email address for their own account
if ($this->getUserId() === $confirmationData['user_id']) {
// immediately update the email address in the current session as well
$this->setEmail($confirmationData['email']);
$_SESSION[self::SESSION_FIELD_EMAIL] = $confirmationData['email'];
}
}
@@ -1318,15 +1291,6 @@ final class Auth extends UserManager {
}
}
/**
* Sets whether the user is currently logged in and updates the session
*
* @param bool $loggedIn whether the user is logged in or not
*/
private function setLoggedIn($loggedIn) {
$_SESSION[self::SESSION_FIELD_LOGGED_IN] = $loggedIn;
}
/**
* Returns whether the user is currently logged in by reading from the session
*
@@ -1345,15 +1309,6 @@ final class Auth extends UserManager {
return $this->isLoggedIn();
}
/**
* Sets the currently signed-in user's ID and updates the session
*
* @param int $userId the user's ID
*/
private function setUserId($userId) {
$_SESSION[self::SESSION_FIELD_USER_ID] = (int) $userId;
}
/**
* Returns the currently signed-in user's ID by reading from the session
*
@@ -1377,15 +1332,6 @@ final class Auth extends UserManager {
return $this->getUserId();
}
/**
* Sets the currently signed-in user's email address and updates the session
*
* @param string $email the email address
*/
private function setEmail($email) {
$_SESSION[self::SESSION_FIELD_EMAIL] = $email;
}
/**
* Returns the currently signed-in user's email address by reading from the session
*
@@ -1400,15 +1346,6 @@ final class Auth extends UserManager {
}
}
/**
* Sets the currently signed-in user's display name and updates the session
*
* @param string $username the display name
*/
private function setUsername($username) {
$_SESSION[self::SESSION_FIELD_USERNAME] = $username;
}
/**
* Returns the currently signed-in user's display name by reading from the session
*
@@ -1423,24 +1360,6 @@ final class Auth extends UserManager {
}
}
/**
* Sets the currently signed-in user's status and updates the session
*
* @param int $status the status as one of the constants from the {@see Status} class
*/
private function setStatus($status) {
$_SESSION[self::SESSION_FIELD_STATUS] = (int) $status;
}
/**
* Sets the currently signed-in user's roles and updates the session
*
* @param int $roles the bitmask containing the roles
*/
private function setRoles($roles) {
$_SESSION[self::SESSION_FIELD_ROLES] = (int) $roles;
}
/**
* Returns the currently signed-in user's status by reading from the session
*
@@ -1582,15 +1501,6 @@ final class Auth extends UserManager {
return true;
}
/**
* Sets whether the currently signed-in user has been remembered by a long-lived cookie
*
* @param bool $remembered whether the user was remembered
*/
private function setRemembered($remembered) {
$_SESSION[self::SESSION_FIELD_REMEMBERED] = $remembered;
}
/**
* Returns whether the currently signed-in user has been remembered by a long-lived cookie
*

View File

@@ -9,6 +9,7 @@
namespace Delight\Auth;
use Delight\Base64\Base64;
use Delight\Cookie\Session;
use Delight\Db\PdoDatabase;
use Delight\Db\PdoDsn;
use Delight\Db\Throwable\Error;
@@ -23,6 +24,20 @@ require_once __DIR__ . '/Exceptions.php';
*/
abstract class UserManager {
/** @var string session field for whether the client is currently signed in */
const SESSION_FIELD_LOGGED_IN = 'auth_logged_in';
/** @var string session field for the ID of the user who is currently signed in (if any) */
const SESSION_FIELD_USER_ID = 'auth_user_id';
/** @var string session field for the email address of the user who is currently signed in (if any) */
const SESSION_FIELD_EMAIL = 'auth_email';
/** @var string session field for the display name (if any) of the user who is currently signed in (if any) */
const SESSION_FIELD_USERNAME = 'auth_username';
/** @var string session field for the status of the user who is currently signed in (if any) as one of the constants from the {@see Status} class */
const SESSION_FIELD_STATUS = 'auth_status';
/** @var string session field for the roles of the user who is currently signed in (if any) as a bitmask using constants from the {@see Role} class */
const SESSION_FIELD_ROLES = 'auth_roles';
/** @var string session field for whether the user who is currently signed in (if any) has been remembered (instead of them having authenticated actively) */
const SESSION_FIELD_REMEMBERED = 'auth_remembered';
const CONFIRMATION_REQUESTS_TTL_IN_SECONDS = 60 * 60 * 24;
/** @var PdoDatabase the database connection to operate on */
@@ -166,6 +181,33 @@ abstract class UserManager {
return $newUserId;
}
/**
* Called when a user has successfully logged in
*
* This may happen via the standard login, via the "remember me" feature, or due to impersonation by administrators
*
* @param int $userId the ID of the user
* @param string $email the email address of the user
* @param string $username the display name (if any) of the user
* @param int $status the status of the user as one of the constants from the {@see Status} class
* @param int $roles the roles of the user as a bitmask using constants from the {@see Role} class
* @param bool $remembered whether the user has been remembered (instead of them having authenticated actively)
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function onLoginSuccessful($userId, $email, $username, $status, $roles, $remembered) {
// re-generate the session ID to prevent session fixation attacks (requests a cookie to be written on the client)
Session::regenerate(true);
// save the user data in the session variables maintained by this library
$_SESSION[self::SESSION_FIELD_LOGGED_IN] = true;
$_SESSION[self::SESSION_FIELD_USER_ID] = (int) $userId;
$_SESSION[self::SESSION_FIELD_EMAIL] = $email;
$_SESSION[self::SESSION_FIELD_USERNAME] = $username;
$_SESSION[self::SESSION_FIELD_STATUS] = (int) $status;
$_SESSION[self::SESSION_FIELD_ROLES] = (int) $roles;
$_SESSION[self::SESSION_FIELD_REMEMBERED] = $remembered;
}
/**
* Returns the requested user data for the account with the specified username (if any)
*

View File

@@ -84,7 +84,7 @@ function processRequestData(\Delight\Auth\Auth $auth) {
return 'wrong password';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email not verified';
return 'email address not verified';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
@@ -242,7 +242,7 @@ function processRequestData(\Delight\Auth\Auth $auth) {
return 'invalid email address';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email not verified';
return 'email address not verified';
}
catch (\Delight\Auth\ResetDisabledException $e) {
return 'password reset disabled';
@@ -422,7 +422,7 @@ function processRequestData(\Delight\Auth\Auth $auth) {
}
}
else {
return 'either ID, email or username required';
return 'either ID, email address or username required';
}
return 'ok';
@@ -457,7 +457,7 @@ function processRequestData(\Delight\Auth\Auth $auth) {
}
}
else {
return 'either ID, email or username required';
return 'either ID, email address or username required';
}
}
else {
@@ -496,7 +496,7 @@ function processRequestData(\Delight\Auth\Auth $auth) {
}
}
else {
return 'either ID, email or username required';
return 'either ID, email address or username required';
}
}
else {
@@ -523,6 +523,63 @@ function processRequestData(\Delight\Auth\Auth $auth) {
return 'ID required';
}
}
else if ($_POST['action'] === 'admin.logInAsUserById') {
if (isset($_POST['id'])) {
try {
$auth->admin()->logInAsUserById($_POST['id']);
return 'ok';
}
catch (\Delight\Auth\UnknownIdException $e) {
return 'unknown ID';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email address not verified';
}
}
else {
return 'ID required';
}
}
else if ($_POST['action'] === 'admin.logInAsUserByEmail') {
if (isset($_POST['email'])) {
try {
$auth->admin()->logInAsUserByEmail($_POST['email']);
return 'ok';
}
catch (\Delight\Auth\InvalidEmailException $e) {
return 'unknown email address';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email address not verified';
}
}
else {
return 'Email address required';
}
}
else if ($_POST['action'] === 'admin.logInAsUserByUsername') {
if (isset($_POST['username'])) {
try {
$auth->admin()->logInAsUserByUsername($_POST['username']);
return 'ok';
}
catch (\Delight\Auth\UnknownUsernameException $e) {
return 'unknown username';
}
catch (\Delight\Auth\AmbiguousUsernameException $e) {
return 'ambiguous username';
}
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email address not verified';
}
}
else {
return 'Username required';
}
}
else {
throw new Exception('Unexpected action: ' . $_POST['action']);
}
@@ -687,7 +744,7 @@ function showGuestUserForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="login" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<input type="text" name="password" placeholder="Password" /> ';
echo '<select name="remember" size="1">';
echo '<option value="0">Remember (keep logged in)? — No</option>';
@@ -709,7 +766,7 @@ function showGuestUserForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="register" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<input type="text" name="password" placeholder="Password" /> ';
echo '<input type="text" name="username" placeholder="Username (optional)" /> ';
echo '<select name="require_verification" size="1">';
@@ -727,7 +784,7 @@ function showGuestUserForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="forgotPassword" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<button type="submit">Forgot password</button>';
echo '</form>';
@@ -743,7 +800,7 @@ function showGuestUserForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.createUser" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<input type="text" name="password" placeholder="Password" /> ';
echo '<input type="text" name="username" placeholder="Username (optional)" /> ';
echo '<select name="require_unique_username" size="1">';
@@ -761,7 +818,7 @@ function showGuestUserForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.deleteUser" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<button type="submit">Delete user by email</button>';
echo '</form>';
@@ -780,7 +837,7 @@ function showGuestUserForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.addRole" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Add role for user by email</button>';
echo '</form>';
@@ -801,7 +858,7 @@ function showGuestUserForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.removeRole" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Remove role for user by email</button>';
echo '</form>';
@@ -819,6 +876,24 @@ function showGuestUserForm() {
echo '<select name="role">' . \createRolesOptions() . '</select>';
echo '<button type="submit">Does user have role?</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.logInAsUserById" />';
echo '<input type="text" name="id" placeholder="ID" /> ';
echo '<button type="submit">Log in as user by ID</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.logInAsUserByEmail" />';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<button type="submit">Log in as user by email address</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.logInAsUserByUsername" />';
echo '<input type="text" name="username" placeholder="Username" /> ';
echo '<button type="submit">Log in as user by username</button>';
echo '</form>';
}
function showConfirmEmailForm() {
@@ -836,7 +911,7 @@ function showConfirmEmailForm() {
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="resendConfirmationForEmail" />';
echo '<input type="text" name="email" placeholder="Email" /> ';
echo '<input type="text" name="email" placeholder="Email address" /> ';
echo '<button type="submit">Re-send confirmation</button>';
echo '</form>';