Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ During development, you may want to disable the request limiting or throttling p

During the lifetime of a session, some user data may be changed remotely, either by a client in another session or by an administrator. That means this information must be regularly resynchronized with its authoritative source in the database, which this library does automatically. By default, this happens every five minutes. If you want to change this interval, pass a custom interval in seconds to the constructor as the fifth argument, which is named `$sessionResyncInterval`.

If all your database tables need a common database name, schema name, or other qualifier that must be specified explicitly, you can optionally pass that qualifier to the constructor as the sixth parameter, which is named `$dbSchema`.

### Registration (sign up)

```php
Expand Down
17 changes: 11 additions & 6 deletions src/Administration.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@
/** Component that can be used for administrative tasks by privileged and authorized users */
final class Administration extends UserManager {

/** @var bool whether email verification is nessecary to login*/
private $requireEmailVerification;

/**
* @param PdoDatabase|PdoDsn|\PDO $databaseConnection the database connection to operate on
* @param string|null $dbTablePrefix (optional) the prefix for the names of all database tables used by this component
* @param string|null $dbSchema (optional) the schema name for all database tables used by this component
* @param bool $requireEmailVerification (optional) whether email verification is nessecary to login
*/
public function __construct($databaseConnection, $dbTablePrefix = null, $dbSchema = null) {
public function __construct($databaseConnection, $dbTablePrefix = null, $dbSchema = null, $requireEmailVerification = true) {
parent::__construct($databaseConnection, $dbTablePrefix, $dbSchema);
$this->requireEmailVerification = $requireEmailVerification;
}

/**
Expand Down Expand Up @@ -326,7 +331,7 @@ function ($each) use ($rolesBitmask) {
*
* @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 EmailNotVerifiedException if `Auth()->requireEmailVerification !== false` and 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) {
Expand All @@ -342,7 +347,7 @@ public function logInAsUserById($id) {
*
* @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 EmailNotVerifiedException if `Auth()->requireEmailVerification !== false` and 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) {
Expand All @@ -361,7 +366,7 @@ public function logInAsUserByEmail($email) {
* @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 EmailNotVerifiedException if `Auth()->requireEmailVerification !== false` and 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) {
Expand Down Expand Up @@ -544,7 +549,7 @@ function ($oldRolesBitmask) use ($role) {
* @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 EmailNotVerifiedException if `Auth()->requireEmailVerification !== false` and 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) {
Expand All @@ -563,7 +568,7 @@ private function logInAsUserByColumnValue($columnName, $columnValue) {
if ($numberOfMatchingUsers === 1) {
$user = $users[0];

if ((int) $user['verified'] === 1) {
if (!$this->requireEmailVerification || (int) $user['verified'] === 1) {
$this->onLoginSuccessful($user['id'], $user['email'], $user['username'], $user['status'], $user['roles_mask'], \PHP_INT_MAX, false);
}
else {
Expand Down
24 changes: 14 additions & 10 deletions src/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ final class Auth extends UserManager {
private $sessionResyncInterval;
/** @var string the name of the cookie used for the 'remember me' feature */
private $rememberCookieName;
/** @var bool whether email verification is nessecary to login*/
private $requireEmailVerification;

/**
* @param PdoDatabase|PdoDsn|\PDO $databaseConnection the database connection to operate on
Expand All @@ -40,12 +42,14 @@ final class Auth extends UserManager {
* @param bool|null $throttling (optional) whether throttling should be enabled (e.g. in production) or disabled (e.g. during development)
* @param int|null $sessionResyncInterval (optional) the interval in seconds after which to resynchronize the session data with its authoritative source in the database
* @param string|null $dbSchema (optional) the schema name for all database tables used by this component
* @param bool $requireEmailVerification (optional) whether email verification is nessecary to login
*/
public function __construct($databaseConnection, $ipAddress = null, $dbTablePrefix = null, $throttling = null, $sessionResyncInterval = null, $dbSchema = null) {
public function __construct($databaseConnection, $ipAddress = null, $dbTablePrefix = null, $throttling = null, $sessionResyncInterval = null, $dbSchema = null, $requireEmailVerification = true) {
parent::__construct($databaseConnection, $dbTablePrefix, $dbSchema);

$this->ipAddress = !empty($ipAddress) ? $ipAddress : (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null);
$this->throttling = isset($throttling) ? (bool) $throttling : true;
$this->requireEmailVerification = (bool) $requireEmailVerification;
$this->sessionResyncInterval = isset($sessionResyncInterval) ? ((int) $sessionResyncInterval) : (60 * 5);
$this->rememberCookieName = self::createRememberCookieName();

Expand Down Expand Up @@ -288,7 +292,7 @@ public function registerWithUniqueUsername($email, $password, $username = null,
* @param callable|null $onBeforeSuccess (optional) a function that receives the user's ID as its single parameter and is executed before successful authentication; must return `true` to proceed or `false` to cancel
* @throws InvalidEmailException if the email address was invalid or could not be found
* @throws InvalidPasswordException if the password was invalid
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
* @throws EmailNotVerifiedException if `Auth()->requireEmailVerification !== false` and if the email address has not been verified yet via confirmation email
* @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
Expand All @@ -313,7 +317,7 @@ public function login($email, $password, $rememberDuration = null, callable $onB
* @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 EmailNotVerifiedException if `Auth()->requireEmailVerification !== false` and if the email address has not been verified yet via confirmation email
* @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
Expand Down Expand Up @@ -802,7 +806,7 @@ public function changePasswordWithoutOldPassword($newPassword) {
* @param callable $callback the function that sends the confirmation email to the user
* @throws InvalidEmailException if the desired new email address is invalid
* @throws UserAlreadyExistsException if a user with the desired new email address already exists
* @throws EmailNotVerifiedException if the current (old) email address has not been verified yet
* @throws EmailNotVerifiedException if `Auth()->requireEmailVerification !== false` and if the current (old) email address has not been verified yet
* @throws NotLoggedInException if the user is not currently signed in
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
Expand Down Expand Up @@ -841,7 +845,7 @@ public function changeEmail($newEmail, callable $callback) {
}

// ensure that at least the current (old) email address has been verified before proceeding
if ((int) $verified !== 1) {
if ($this->requireEmailVerification && (int) $verified !== 1) {
throw new EmailNotVerifiedException();
}

Expand Down Expand Up @@ -958,7 +962,7 @@ private function resendConfirmationForColumnValue($columnName, $columnValue, cal
* @param int|null $requestExpiresAfter (optional) the interval in seconds after which the request should expire
* @param int|null $maxOpenRequests (optional) the maximum number of unexpired and unused requests per user
* @throws InvalidEmailException if the email address was invalid or could not be found
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
* @throws EmailNotVerifiedException if `Auth()->requireEmailVerification !== false` and if the email address has not been verified yet via confirmation email
* @throws ResetDisabledException if the user has explicitly disabled password resets for their account
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
Expand Down Expand Up @@ -990,7 +994,7 @@ public function forgotPassword($email, callable $callback, $requestExpiresAfter
);

// ensure that the account has been verified before initiating a password reset
if ((int) $userData['verified'] !== 1) {
if ($this->requireEmailVerification && (int) $userData['verified'] !== 1) {
throw new EmailNotVerifiedException();
}

Expand Down Expand Up @@ -1024,7 +1028,7 @@ public function forgotPassword($email, callable $callback, $requestExpiresAfter
* @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 EmailNotVerifiedException if `Auth()->requireEmailVerification !== false` and if the email address has not been verified yet via confirmation email
* @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
Expand Down Expand Up @@ -1068,7 +1072,7 @@ private function authenticateUserInternal($password, $email = null, $username =
$this->updatePasswordInternal($userData['id'], $password);
}

if ((int) $userData['verified'] === 1) {
if (!$this->requireEmailVerification || (int) $userData['verified'] === 1) {
if (!isset($onBeforeSuccess) || (\is_callable($onBeforeSuccess) && $onBeforeSuccess($userData['id']) === true)) {
$this->onLoginSuccessful($userData['id'], $userData['email'], $userData['username'], $userData['status'], $userData['roles_mask'], $userData['force_logout'], false);

Expand Down Expand Up @@ -1773,7 +1777,7 @@ public function throttle(array $criteria, $supply, $interval, $burstiness = null
* @return Administration
*/
public function admin() {
return new Administration($this->db, $this->dbTablePrefix, $this->dbSchema);
return new Administration($this->db, $this->dbTablePrefix, $this->dbSchema, $this->requireEmailVerification);
}

/**
Expand Down
41 changes: 40 additions & 1 deletion src/UserManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,45 @@ protected function onLoginSuccessful($userId, $email, $username, $status, $roles
$_SESSION[self::SESSION_FIELD_LAST_RESYNC] = \time();
}


/**
* 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 $userId the user id 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)
*/
public function getUserDataById($userId, array $requestedColumns) {
try {
$projection = \implode(', ', $requestedColumns);

$users = $this->db->select(
'SELECT ' . $projection . ' FROM ' . $this->makeTableName('users') . ' WHERE id = ? LIMIT 2 OFFSET 0',
[ $userId ]
);
}
catch (Error $e) {
throw new DatabaseError($e->getMessage());
}

if (empty($users)) {
throw new UnknownUsernameException();
}
else {
if (\count($users) === 1) {
return $users[0];
}
else {
throw new AmbiguousUsernameException();
}
}
}

/**
* Returns the requested user data for the account with the specified username (if any)
*
Expand All @@ -257,7 +296,7 @@ protected function onLoginSuccessful($userId, $email, $username, $status, $roles
* @throws AmbiguousUsernameException if multiple users with the specified username have been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function getUserDataByUsername($username, array $requestedColumns) {
public function getUserDataByUsername($username, array $requestedColumns) {
try {
$projection = \implode(', ', $requestedColumns);

Expand Down