mirror of
https://github.com/delight-im/PHP-Auth.git
synced 2025-08-24 08:32:59 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
8acd3a9779 | ||
|
374f27176b | ||
|
3cb2284870 | ||
|
690485ba6d | ||
|
495a87d499 | ||
|
784030139b | ||
|
fb6f3d31b8 | ||
|
370ecc4933 | ||
|
da2d282648 | ||
|
4aaf85e3cf | ||
|
f2561a1932 | ||
|
8cc54473e3 | ||
|
f26f2209cd | ||
|
188086f2e4 | ||
|
c6213a6081 | ||
|
c55250c572 |
@@ -137,6 +137,8 @@ catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
}
|
||||
```
|
||||
|
||||
If you want to sign in with usernames on the other hand, either in addition to the login via email address or as a replacement, that's possible as well. Simply call the method `loginWithUsername` instead of method `login`. Then, instead of catching `InvalidEmailException`, make sure to catch both `UnknownUsernameException` and `AmbiguousUsernameException`. You may also want to read the notes about the uniqueness of usernames in the section that explains how to [sign up new users](#registration-sign-up).
|
||||
|
||||
### Email verification
|
||||
|
||||
Extract the selector and token from the URL that the user clicked on in the verification email.
|
||||
|
261
src/Auth.php
261
src/Auth.php
@@ -254,7 +254,7 @@ class Auth {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to sign in a user
|
||||
* Attempts to sign in a user with their email address and password
|
||||
*
|
||||
* @param string $email the user's email address
|
||||
* @param string $password the user's password
|
||||
@@ -265,61 +265,27 @@ class Auth {
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
public function login($email, $password, $rememberDuration = null) {
|
||||
$email = self::validateEmailAddress($email);
|
||||
$password = self::validatePassword($password);
|
||||
$this->authenticateUserInternal($password, $email, null, $rememberDuration);
|
||||
}
|
||||
|
||||
try {
|
||||
$userData = $this->db->selectRow(
|
||||
'SELECT id, password, verified, username FROM users WHERE email = ?',
|
||||
[ $email ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if (!empty($userData)) {
|
||||
if (password_verify($password, $userData['password'])) {
|
||||
// if the password needs to be re-hashed to keep up with improving password cracking techniques
|
||||
if (password_needs_rehash($userData['password'], PASSWORD_DEFAULT)) {
|
||||
// create a new hash from the password and update it in the database
|
||||
$this->updatePassword($userData['id'], $password);
|
||||
}
|
||||
|
||||
if ($userData['verified'] === 1) {
|
||||
$this->onLoginSuccessful($userData['id'], $email, $userData['username'], false);
|
||||
|
||||
// continue to support the old parameter format
|
||||
if ($rememberDuration === true) {
|
||||
$rememberDuration = 60 * 60 * 24 * 28;
|
||||
}
|
||||
elseif ($rememberDuration === false) {
|
||||
$rememberDuration = null;
|
||||
}
|
||||
|
||||
if ($rememberDuration !== null) {
|
||||
$this->createRememberDirective($userData['id'], $rememberDuration);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else {
|
||||
throw new EmailNotVerifiedException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN);
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
|
||||
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN);
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
|
||||
|
||||
throw new InvalidEmailException();
|
||||
}
|
||||
/**
|
||||
* Attempts to sign in a user with their username and password
|
||||
*
|
||||
* When using this method to authenticate users, you should ensure that usernames are unique
|
||||
*
|
||||
* Consistently using {@see registerWithUniqueUsername} instead of {@see register} can be helpful
|
||||
*
|
||||
* @param string $username the user's username
|
||||
* @param string $password the user's password
|
||||
* @param int|bool|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
|
||||
* @throws UnknownUsernameException if the specified username does not exist
|
||||
* @throws AmbiguousUsernameException if the specified username is ambiguous, i.e. there are multiple users with that name
|
||||
* @throws InvalidPasswordException if the password was invalid
|
||||
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
public function loginWithUsername($username, $password, $rememberDuration = null) {
|
||||
$this->authenticateUserInternal($password, null, $username, $rememberDuration);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -765,18 +731,27 @@ class Auth {
|
||||
|
||||
$username = isset($username) ? trim($username) : null;
|
||||
|
||||
// if the supplied username is the empty string or has consisted of whitespace only
|
||||
if ($username === '') {
|
||||
// this actually means that there is no username
|
||||
$username = null;
|
||||
}
|
||||
|
||||
// if the uniqueness of the username is to be ensured
|
||||
if ($requireUniqueUsername) {
|
||||
// count the number of users who do already have that specified username
|
||||
$occurrencesOfUsername = $this->db->selectValue(
|
||||
'SELECT COUNT(*) FROM users WHERE username = ?',
|
||||
[ $username ]
|
||||
);
|
||||
// if a username has actually been provided
|
||||
if ($username !== null) {
|
||||
// count the number of users who do already have that specified username
|
||||
$occurrencesOfUsername = $this->db->selectValue(
|
||||
'SELECT COUNT(*) FROM users WHERE username = ?',
|
||||
[ $username ]
|
||||
);
|
||||
|
||||
// if any user with that username does already exist
|
||||
if ($occurrencesOfUsername > 0) {
|
||||
// cancel the operation and report the violation of this requirement
|
||||
throw new DuplicateUsernameException();
|
||||
// if any user with that username does already exist
|
||||
if ($occurrencesOfUsername > 0) {
|
||||
// cancel the operation and report the violation of this requirement
|
||||
throw new DuplicateUsernameException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,20 +787,135 @@ class Auth {
|
||||
return $newUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates an existing user
|
||||
*
|
||||
* @param string $password the user's password
|
||||
* @param string|null $email (optional) the user's email address
|
||||
* @param string|null $username (optional) the user's username
|
||||
* @param int|bool|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
|
||||
* @throws InvalidEmailException if the email address was invalid or could not be found
|
||||
* @throws UnknownUsernameException if an attempt has been made to authenticate with a non-existing username
|
||||
* @throws AmbiguousUsernameException if an attempt has been made to authenticate with an ambiguous username
|
||||
* @throws InvalidPasswordException if the password was invalid
|
||||
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
private function authenticateUserInternal($password, $email = null, $username = null, $rememberDuration = null) {
|
||||
if ($email !== null) {
|
||||
$email = self::validateEmailAddress($email);
|
||||
|
||||
// attempt to look up the account information using the specified email address
|
||||
try {
|
||||
$userData = $this->getUserDataByEmailAddress(
|
||||
$email,
|
||||
[ 'id', 'email', 'password', 'verified', 'username' ]
|
||||
);
|
||||
}
|
||||
// if there is no user with the specified email address
|
||||
catch (InvalidEmailException $e) {
|
||||
// throttle this operation
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN);
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
|
||||
|
||||
// and re-throw the exception
|
||||
throw new InvalidEmailException();
|
||||
}
|
||||
}
|
||||
elseif ($username !== null) {
|
||||
$username = trim($username);
|
||||
|
||||
// attempt to look up the account information using the specified username
|
||||
try {
|
||||
$userData = $this->getUserDataByUsername(
|
||||
$username,
|
||||
[ 'id', 'email', 'password', 'verified', 'username' ]
|
||||
);
|
||||
}
|
||||
// if there is no user with the specified username
|
||||
catch (UnknownUsernameException $e) {
|
||||
// throttle this operation
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN);
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $username);
|
||||
|
||||
// and re-throw the exception
|
||||
throw new UnknownUsernameException();
|
||||
}
|
||||
// if there are multiple users with the specified username
|
||||
catch (AmbiguousUsernameException $e) {
|
||||
// throttle this operation
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN);
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $username);
|
||||
|
||||
// and re-throw the exception
|
||||
throw new AmbiguousUsernameException();
|
||||
}
|
||||
}
|
||||
// if neither an email address nor a username has been provided
|
||||
else {
|
||||
// we can't do anything here because the method call has been invalid
|
||||
throw new EmailOrUsernameRequiredError();
|
||||
}
|
||||
|
||||
$password = self::validatePassword($password);
|
||||
|
||||
if (password_verify($password, $userData['password'])) {
|
||||
// if the password needs to be re-hashed to keep up with improving password cracking techniques
|
||||
if (password_needs_rehash($userData['password'], PASSWORD_DEFAULT)) {
|
||||
// create a new hash from the password and update it in the database
|
||||
$this->updatePassword($userData['id'], $password);
|
||||
}
|
||||
|
||||
if ($userData['verified'] === 1) {
|
||||
$this->onLoginSuccessful($userData['id'], $userData['email'], $userData['username'], false);
|
||||
|
||||
// continue to support the old parameter format
|
||||
if ($rememberDuration === true) {
|
||||
$rememberDuration = 60 * 60 * 24 * 28;
|
||||
}
|
||||
elseif ($rememberDuration === false) {
|
||||
$rememberDuration = null;
|
||||
}
|
||||
|
||||
if ($rememberDuration !== null) {
|
||||
$this->createRememberDirective($userData['id'], $rememberDuration);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else {
|
||||
throw new EmailNotVerifiedException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// throttle this operation
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN);
|
||||
if (isset($email)) {
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
|
||||
}
|
||||
elseif (isset($username)) {
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $username);
|
||||
}
|
||||
|
||||
// we cannot authenticate the user due to the password being wrong
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested user data for the account with the specified email address (if any)
|
||||
*
|
||||
* You must never pass untrusted input to the parameter that takes the column list
|
||||
*
|
||||
* @param string $email the email address to look for
|
||||
* @param array $requestColumns the columns to request from the user's record
|
||||
* @param array $requestedColumns the columns to request from the user's record
|
||||
* @return array the user data (if an account was found)
|
||||
* @throws InvalidEmailException if the email address could not be found
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
private function getUserDataByEmailAddress($email, array $requestColumns) {
|
||||
private function getUserDataByEmailAddress($email, array $requestedColumns) {
|
||||
try {
|
||||
$projection = implode(', ', $requestColumns);
|
||||
$projection = implode(', ', $requestedColumns);
|
||||
$userData = $this->db->selectRow(
|
||||
'SELECT ' . $projection . ' FROM users WHERE email = ?',
|
||||
[ $email ]
|
||||
@@ -843,6 +933,43 @@ class Auth {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested user data for the account with the specified username (if any)
|
||||
*
|
||||
* You must never pass untrusted input to the parameter that takes the column list
|
||||
*
|
||||
* @param string $username the username to look for
|
||||
* @param array $requestedColumns the columns to request from the user's record
|
||||
* @return array the user data (if an account was found unambiguously)
|
||||
* @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 AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
private function getUserDataByUsername($username, array $requestedColumns) {
|
||||
try {
|
||||
$projection = implode(', ', $requestedColumns);
|
||||
$users = $this->db->select(
|
||||
'SELECT ' . $projection . ' FROM users WHERE username = ? LIMIT 0, 2',
|
||||
[ $username ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if (empty($users)) {
|
||||
throw new UnknownUsernameException();
|
||||
}
|
||||
else {
|
||||
if (count($users) === 1) {
|
||||
return $users[0];
|
||||
}
|
||||
else {
|
||||
throw new AmbiguousUsernameException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of open requests for a password reset by the specified user
|
||||
*
|
||||
|
@@ -12,6 +12,8 @@ class AuthException extends \Exception {}
|
||||
|
||||
class InvalidEmailException extends AuthException {}
|
||||
|
||||
class UnknownUsernameException extends AuthException {}
|
||||
|
||||
class InvalidPasswordException extends AuthException {}
|
||||
|
||||
class EmailNotVerifiedException extends AuthException {}
|
||||
@@ -28,6 +30,8 @@ class TooManyRequestsException extends AuthException {}
|
||||
|
||||
class DuplicateUsernameException extends AuthException {}
|
||||
|
||||
class AmbiguousUsernameException extends AuthException {}
|
||||
|
||||
class AuthError extends \Exception {}
|
||||
|
||||
class DatabaseError extends AuthError {}
|
||||
@@ -35,3 +39,5 @@ class DatabaseError extends AuthError {}
|
||||
class MissingCallbackError extends AuthError {}
|
||||
|
||||
class HeadersAlreadySentError extends AuthError {}
|
||||
|
||||
class EmailOrUsernameRequiredError extends AuthError {}
|
||||
|
@@ -48,13 +48,27 @@ function processRequestData(\Delight\Auth\Auth $auth) {
|
||||
}
|
||||
|
||||
try {
|
||||
$auth->login($_POST['email'], $_POST['password'], $rememberDuration);
|
||||
if (isset($_POST['email'])) {
|
||||
$auth->login($_POST['email'], $_POST['password'], $rememberDuration);
|
||||
}
|
||||
elseif (isset($_POST['username'])) {
|
||||
$auth->loginWithUsername($_POST['username'], $_POST['password'], $rememberDuration);
|
||||
}
|
||||
else {
|
||||
return 'either email address or username required';
|
||||
}
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
catch (\Delight\Auth\InvalidEmailException $e) {
|
||||
return 'wrong email address';
|
||||
}
|
||||
catch (\Delight\Auth\UnknownUsernameException $e) {
|
||||
return 'unknown username';
|
||||
}
|
||||
catch (\Delight\Auth\AmbiguousUsernameException $e) {
|
||||
return 'ambiguous username';
|
||||
}
|
||||
catch (\Delight\Auth\InvalidPasswordException $e) {
|
||||
return 'wrong password';
|
||||
}
|
||||
@@ -86,7 +100,16 @@ function processRequestData(\Delight\Auth\Auth $auth) {
|
||||
$callback = null;
|
||||
}
|
||||
|
||||
return $auth->register($_POST['email'], $_POST['password'], $_POST['username'], $callback);
|
||||
if (!isset($_POST['require_unique_username'])) {
|
||||
$_POST['require_unique_username'] = '0';
|
||||
}
|
||||
|
||||
if ($_POST['require_unique_username'] == 0) {
|
||||
return $auth->register($_POST['email'], $_POST['password'], $_POST['username'], $callback);
|
||||
}
|
||||
else {
|
||||
return $auth->registerWithUniqueUsername($_POST['email'], $_POST['password'], $_POST['username'], $callback);
|
||||
}
|
||||
}
|
||||
catch (\Delight\Auth\InvalidEmailException $e) {
|
||||
return 'invalid email address';
|
||||
@@ -95,7 +118,10 @@ function processRequestData(\Delight\Auth\Auth $auth) {
|
||||
return 'invalid password';
|
||||
}
|
||||
catch (\Delight\Auth\UserAlreadyExistsException $e) {
|
||||
return 'user already exists';
|
||||
return 'email address already exists';
|
||||
}
|
||||
catch (\Delight\Auth\DuplicateUsernameException $e) {
|
||||
return 'username already exists';
|
||||
}
|
||||
catch (\Delight\Auth\TooManyRequestsException $e) {
|
||||
return 'too many requests';
|
||||
@@ -263,7 +289,18 @@ function showGuestUserForm() {
|
||||
echo '<option value="0">Remember (keep logged in)? — No</option>';
|
||||
echo '<option value="1">Remember (keep logged in)? — Yes</option>';
|
||||
echo '</select> ';
|
||||
echo '<button type="submit">Login</button>';
|
||||
echo '<button type="submit">Log in with email address</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
echo '<input type="hidden" name="action" value="login" />';
|
||||
echo '<input type="text" name="username" placeholder="Username" /> ';
|
||||
echo '<input type="text" name="password" placeholder="Password" /> ';
|
||||
echo '<select name="remember" size="1">';
|
||||
echo '<option value="0">Remember (keep logged in)? — No</option>';
|
||||
echo '<option value="1">Remember (keep logged in)? — Yes</option>';
|
||||
echo '</select> ';
|
||||
echo '<button type="submit">Log in with username</button>';
|
||||
echo '</form>';
|
||||
|
||||
echo '<form action="" method="post" accept-charset="utf-8">';
|
||||
@@ -275,6 +312,10 @@ function showGuestUserForm() {
|
||||
echo '<option value="0">Require email confirmation? — No</option>';
|
||||
echo '<option value="1">Require email confirmation? — Yes</option>';
|
||||
echo '</select> ';
|
||||
echo '<select name="require_unique_username" size="1">';
|
||||
echo '<option value="0">Username — Any</option>';
|
||||
echo '<option value="1">Username — Unique</option>';
|
||||
echo '</select> ';
|
||||
echo '<button type="submit">Register</button>';
|
||||
echo '</form>';
|
||||
|
||||
|
Reference in New Issue
Block a user