1
0
mirror of https://github.com/delight-im/PHP-Auth.git synced 2025-08-08 09:06:29 +02:00

26 Commits

Author SHA1 Message Date
Marco
095b8ccc70 Document 'changePasswordForUserById' from class 'Administration' 2018-03-21 03:24:06 +01:00
Marco
550a6d0355 Add tests for 'changePasswordForUserById' from class 'Administration' 2018-03-21 03:22:29 +01:00
Marco
c494e0fa13 Throw 'UnknownIdException' in 'updatePasswordInternal' when no matches 2018-03-21 03:20:11 +01:00
Marco
d7d9899167 Use 'changePasswordForUserById' for 'changePasswordForUserByUsername' 2018-03-21 02:55:31 +01:00
Marco
05165a44a6 Implement method 'changePasswordForUserById' in class 'Administration' 2018-03-21 02:54:50 +01:00
Marco
c3f2097750 Document 'changePasswordForUserByUsername' from 'Administration' 2018-03-21 02:35:09 +01:00
Marco
395a065fd4 Add tests for 'changePasswordForUserByUsername' from 'Administration' 2018-03-21 02:28:55 +01:00
Marco
627c592891 Let 'Administration' constructor be part of public API 2018-03-20 16:13:56 +01:00
Marco
2a6d1c4f7d Delete 'remember me' directives in 'changePasswordForUserByUsername' 2018-03-20 16:11:56 +01:00
Marco
a63e5ec053 Move essence of 'deleteRememberDirectiveForUserById' to 'UserManager' 2018-03-20 16:09:25 +01:00
Marco
4115340927 Improve language 2018-03-20 16:04:29 +01:00
Marco
09dac6a5f5 Rename method 'deleteRememberDirective' in class 'Auth'
Use more expressive name 'deleteRememberDirectiveForUserById'
2018-03-20 15:57:37 +01:00
Marco
3a7a860c6d Validate password in 'changePasswordForUserByUsername' for consistency 2018-03-20 15:54:19 +01:00
maxsenft
131aea3ded Implement method 'changePasswordForUserByUsername' in 'Administration' 2018-03-20 15:50:44 +01:00
maxsenft
e14f3d1925 Rename method 'updatePassword' to 'updatePasswordInternal' 2018-03-20 15:45:25 +01:00
maxsenft
1d54ff2f6b Move 'updatePassword' method from class 'Auth' to class 'UserManager' 2018-03-20 15:41:57 +01:00
maxsenft
ec6afdad48 Accept 'PdoDsn' and 'PDO' as well in 'Administration' constructor 2018-03-20 15:38:35 +01:00
Marco
58e69fdd0e Do not pass 'null' to 'count' which triggers a warning since PHP 7.2 2018-03-15 23:32:15 +01:00
Marco
e7e174b05d Only configure and start session if not already started 2018-03-12 22:29:56 +01:00
Marco
8f35cc9965 Optimize spacing in PostgreSQL schema 2018-03-12 18:44:32 +01:00
Marco
142ccc362f Shorten line of text in README for better overview 2018-03-12 02:18:44 +01:00
Marco
bce31f9cfc Link to MariaDB schema separately from MySQL in README 2018-03-12 02:15:35 +01:00
Marco
3ddc7af1b4 Document support for PostgreSQL 2018-03-12 02:11:54 +01:00
Marco
62d9e44aa4 Add check constraints for unsigned integers in PostgreSQL schema 2018-03-12 01:51:33 +01:00
Marco
1121685cef Improve database schema for PostgreSQL 2018-03-12 01:51:15 +01:00
Tiberiu Chibici
2f9bab4779 Add database schema for PostgreSQL 2018-03-12 00:32:53 +01:00
6 changed files with 260 additions and 61 deletions

57
Database/PostgreSQL.sql Normal file
View File

@@ -0,0 +1,57 @@
-- PHP-Auth (https://github.com/delight-im/PHP-Auth)
-- Copyright (c) delight.im (https://www.delight.im/)
-- Licensed under the MIT License (https://opensource.org/licenses/MIT)
BEGIN;
CREATE TABLE IF NOT EXISTS "users" (
"id" SERIAL PRIMARY KEY CHECK ("id" >= 0),
"email" VARCHAR(249) UNIQUE NOT NULL,
"password" VARCHAR(255) NOT NULL,
"username" VARCHAR(100) DEFAULT NULL,
"status" SMALLINT NOT NULL DEFAULT '0' CHECK ("status" >= 0),
"verified" SMALLINT NOT NULL DEFAULT '0' CHECK ("verified" >= 0),
"resettable" SMALLINT NOT NULL DEFAULT '1' CHECK ("resettable" >= 0),
"roles_mask" INTEGER NOT NULL DEFAULT '0' CHECK ("roles_mask" >= 0),
"registered" INTEGER NOT NULL CHECK ("registered" >= 0),
"last_login" INTEGER DEFAULT NULL CHECK ("last_login" >= 0)
);
CREATE TABLE IF NOT EXISTS "users_confirmations" (
"id" SERIAL PRIMARY KEY CHECK ("id" >= 0),
"user_id" INTEGER NOT NULL CHECK ("user_id" >= 0),
"email" VARCHAR(249) NOT NULL,
"selector" VARCHAR(16) UNIQUE NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
);
CREATE INDEX IF NOT EXISTS "email_expires" ON "users_confirmations" ("email", "expires");
CREATE INDEX IF NOT EXISTS "user_id" ON "users_confirmations" ("user_id");
CREATE TABLE IF NOT EXISTS "users_remembered" (
"id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(24) UNIQUE NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
);
CREATE INDEX IF NOT EXISTS "user" ON "users_remembered" ("user");
CREATE TABLE IF NOT EXISTS "users_resets" (
"id" BIGSERIAL PRIMARY KEY CHECK ("id" >= 0),
"user" INTEGER NOT NULL CHECK ("user" >= 0),
"selector" VARCHAR(20) UNIQUE NOT NULL,
"token" VARCHAR(255) NOT NULL,
"expires" INTEGER NOT NULL CHECK ("expires" >= 0)
);
CREATE INDEX IF NOT EXISTS "user_expires" ON "users_resets" ("user", "expires");
CREATE TABLE IF NOT EXISTS "users_throttling" (
"bucket" VARCHAR(44) PRIMARY KEY,
"tokens" REAL NOT NULL CHECK ("tokens" >= 0),
"replenished_at" INTEGER NOT NULL CHECK ("replenished_at" >= 0),
"expires_at" INTEGER NOT NULL CHECK ("expires_at" >= 0)
);
CREATE INDEX IF NOT EXISTS "expires_at" ON "users_throttling" ("expires_at");
COMMIT;

View File

@@ -18,9 +18,9 @@ Completely framework-agnostic and database-agnostic.
* PHP 5.6.0+
* PDO (PHP Data Objects) extension (`pdo`)
* MySQL Native Driver (`mysqlnd`) **or** SQLite driver (`sqlite`)
* MySQL Native Driver (`mysqlnd`) **or** PostgreSQL driver (`pgsql`) **or** SQLite driver (`sqlite`)
* OpenSSL extension (`openssl`)
* MySQL 5.5.3+ **or** MariaDB 5.5.23+ **or** SQLite 3.14.1+ **or** other SQL databases that you create the [schema](Database) for
* MySQL 5.5.3+ **or** MariaDB 5.5.23+ **or** PostgreSQL 9.5.10+ **or** SQLite 3.14.1+ **or** [other SQL databases](Database)
## Installation
@@ -38,7 +38,9 @@ Completely framework-agnostic and database-agnostic.
1. Set up a database and create the required tables:
* [MariaDB](Database/MySQL.sql)
* [MySQL](Database/MySQL.sql)
* [PostgreSQL](Database/PostgreSQL.sql)
* [SQLite](Database/SQLite.sql)
## Upgrading
@@ -80,6 +82,7 @@ Migrating from an earlier version of this project? See our [upgrade guide](Migra
* [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)
* [Changing a users password](#changing-a-users-password)
* [Cookies](#cookies)
* [Renaming the librarys cookies](#renaming-the-librarys-cookies)
* [Defining the domain scope for cookies](#defining-the-domain-scope-for-cookies)
@@ -96,12 +99,16 @@ Migrating from an earlier version of this project? See our [upgrade guide](Migra
```php
// $db = new \PDO('mysql:dbname=my-database;host=localhost;charset=utf8mb4', 'my-username', 'my-password');
// or
// $db = new \PDO('pgsql:dbname=my-database;host=localhost;port=5432', 'my-username', 'my-password');
// or
// $db = new \PDO('sqlite:../Databases/my-database.sqlite');
// or
// $db = new \Delight\Db\PdoDsn('mysql:dbname=my-database;host=localhost;charset=utf8mb4', 'my-username', 'my-password');
// or
// $db = new \Delight\Db\PdoDsn('pgsql:dbname=my-database;host=localhost;port=5432', 'my-username', 'my-password');
// or
// $db = new \Delight\Db\PdoDsn('sqlite:../Databases/my-database.sqlite');
$auth = new \Delight\Auth\Auth($db);
@@ -995,6 +1002,35 @@ catch (\Delight\Auth\EmailNotVerifiedException $e) {
}
```
#### Changing a users password
```php
try {
$auth->admin()->changePasswordForUserById($_POST['id'], $_POST['newPassword']);
}
catch (\Delight\Auth\UnknownIdException $e) {
// unknown ID
}
catch (\Delight\Auth\InvalidPasswordException $e) {
// invalid password
}
// or
try {
$auth->admin()->changePasswordForUserByUsername($_POST['username'], $_POST['newPassword']);
}
catch (\Delight\Auth\UnknownUsernameException $e) {
// unknown username
}
catch (\Delight\Auth\AmbiguousUsernameException $e) {
// ambiguous username
}
catch (\Delight\Auth\InvalidPasswordException $e) {
// invalid password
}
```
### Cookies
This library uses two cookies to keep state on the client: The first, whose name you can retrieve using

View File

@@ -9,6 +9,7 @@
namespace Delight\Auth;
use Delight\Db\PdoDatabase;
use Delight\Db\PdoDsn;
use Delight\Db\Throwable\Error;
require_once __DIR__ . '/Exceptions.php';
@@ -17,12 +18,10 @@ require_once __DIR__ . '/Exceptions.php';
final class Administration extends UserManager {
/**
* @internal
*
* @param PdoDatabase $databaseConnection the database connection to operate on
* @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
*/
public function __construct(PdoDatabase $databaseConnection, $dbTablePrefix = null) {
public function __construct($databaseConnection, $dbTablePrefix = null) {
parent::__construct($databaseConnection, $dbTablePrefix);
}
@@ -370,6 +369,49 @@ final class Administration extends UserManager {
}
}
/**
* Changes the password for the user with the given ID
*
* @param int $userId the ID of the user whose password to change
* @param string $newPassword the new password to set
* @throws UnknownIdException if no user with the specified ID has been found
* @throws InvalidPasswordException if the desired new password has been invalid
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function changePasswordForUserById($userId, $newPassword) {
$userId = (int) $userId;
$newPassword = self::validatePassword($newPassword);
$this->updatePasswordInternal(
$userId,
$newPassword
);
$this->deleteRememberDirectiveForUserById($userId);
}
/**
* Changes the password for the user with the given username
*
* @param string $username the username of the user whose password to change
* @param string $newPassword the new password to set
* @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 InvalidPasswordException if the desired new password has been invalid
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function changePasswordForUserByUsername($username, $newPassword) {
$userData = $this->getUserDataByUsername(
\trim($username),
[ 'id' ]
);
$this->changePasswordForUserById(
(int) $userData['id'],
$newPassword
);
}
/**
* Deletes all existing users where the column with the specified name has the given value
*
@@ -510,7 +552,7 @@ final class Administration extends UserManager {
throw new DatabaseError();
}
$numberOfMatchingUsers = \count($users);
$numberOfMatchingUsers = ($users !== null) ? \count($users) : 0;
if ($numberOfMatchingUsers === 1) {
$user = $users[0];

View File

@@ -48,7 +48,7 @@ final class Auth extends UserManager {
$this->sessionResyncInterval = isset($sessionResyncInterval) ? ((int) $sessionResyncInterval) : (60 * 5);
$this->rememberCookieName = self::createRememberCookieName();
$this->initSession();
$this->initSessionIfNecessary();
$this->enhanceHttpSecurity();
$this->processRememberDirective();
@@ -56,16 +56,18 @@ final class Auth extends UserManager {
}
/** Initializes the session and sets the correct configuration */
private function initSession() {
// use cookies to store session IDs
\ini_set('session.use_cookies', 1);
// use cookies only (do not send session IDs in URLs)
\ini_set('session.use_only_cookies', 1);
// do not send session IDs in URLs
\ini_set('session.use_trans_sid', 0);
private function initSessionIfNecessary() {
if (\session_status() === \PHP_SESSION_NONE) {
// use cookies to store session IDs
\ini_set('session.use_cookies', 1);
// use cookies only (do not send session IDs in URLs)
\ini_set('session.use_only_cookies', 1);
// do not send session IDs in URLs
\ini_set('session.use_trans_sid', 0);
// start the session (requests a cookie to be written on the client)
@Session::start();
// start the session (requests a cookie to be written on the client)
@Session::start();
}
}
/** Improves the application's security over HTTP(S) by setting specific headers */
@@ -377,7 +379,7 @@ final class Auth extends UserManager {
// if a user ID was set
if (isset($userId)) {
// delete any existing remember directives
$this->deleteRememberDirective($userId);
$this->deleteRememberDirectiveForUserById($userId);
}
// remove all session variables maintained by this library
@@ -437,22 +439,8 @@ final class Auth extends UserManager {
$this->setRememberCookie($selector, $token, $expires);
}
/**
* Clears an existing directive that keeps the user logged in ("remember me")
*
* @param int $userId the user ID that shouldn't be kept signed in anymore
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function deleteRememberDirective($userId) {
try {
$this->db->delete(
$this->dbTablePrefix . 'users_remembered',
[ 'user' => $userId ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
protected function deleteRememberDirectiveForUserById($userId) {
parent::deleteRememberDirectiveForUserById($userId);
$this->setRememberCookie(null, null, \time() - 3600);
}
@@ -715,36 +703,14 @@ final class Auth extends UserManager {
if ($this->isLoggedIn()) {
$newPassword = self::validatePassword($newPassword);
$userId = $this->getUserId();
$this->updatePassword($userId, $newPassword);
$this->deleteRememberDirective($userId);
$this->updatePasswordInternal($userId, $newPassword);
$this->deleteRememberDirectiveForUserById($userId);
}
else {
throw new NotLoggedInException();
}
}
/**
* Updates the given user's password by setting it to the new specified password
*
* @param int $userId the ID of the user whose password should be updated
* @param string $newPassword the new password
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function updatePassword($userId, $newPassword) {
$newPassword = \password_hash($newPassword, \PASSWORD_DEFAULT);
try {
$this->db->update(
$this->dbTablePrefix . 'users',
[ 'password' => $newPassword ],
[ 'id' => $userId ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
}
/**
* Attempts to change the email address of the currently signed-in user (which requires confirmation)
*
@@ -1023,7 +989,7 @@ final class Auth extends UserManager {
// 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);
$this->updatePasswordInternal($userData['id'], $password);
}
if ((int) $userData['verified'] === 1) {
@@ -1218,10 +1184,10 @@ final class Auth extends UserManager {
$newPassword = self::validatePassword($newPassword);
// update the password in the database
$this->updatePassword($resetData['user'], $newPassword);
$this->updatePasswordInternal($resetData['user'], $newPassword);
// delete any remaining remember directives
$this->deleteRememberDirective($resetData['user']);
$this->deleteRememberDirectiveForUserById($resetData['user']);
try {
$this->db->delete(

View File

@@ -182,6 +182,33 @@ abstract class UserManager {
return $newUserId;
}
/**
* Updates the given user's password by setting it to the new specified password
*
* @param int $userId the ID of the user whose password should be updated
* @param string $newPassword the new password
* @throws UnknownIdException if no user with the specified ID has been found
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function updatePasswordInternal($userId, $newPassword) {
$newPassword = \password_hash($newPassword, \PASSWORD_DEFAULT);
try {
$affected = $this->db->update(
$this->dbTablePrefix . 'users',
[ 'password' => $newPassword ],
[ 'id' => $userId ]
);
if ($affected === 0) {
throw new UnknownIdException();
}
}
catch (Error $e) {
throw new DatabaseError();
}
}
/**
* Called when a user has successfully logged in
*
@@ -336,4 +363,22 @@ abstract class UserManager {
}
}
/**
* Clears an existing directive that keeps the user logged in ("remember me")
*
* @param int $userId the ID of the user who shouldn't be kept signed in anymore
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
protected function deleteRememberDirectiveForUserById($userId) {
try {
$this->db->delete(
$this->dbTablePrefix . 'users_remembered',
[ 'user' => $userId ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
}
}

View File

@@ -29,6 +29,8 @@ require __DIR__.'/../vendor/autoload.php';
$db = new \PDO('mysql:dbname=php_auth;host=127.0.0.1;charset=utf8mb4', 'root', 'monkey');
// or
// $db = new \PDO('pgsql:dbname=php_auth;host=127.0.0.1;port=5432', 'postgres', 'monkey');
// or
// $db = new \PDO('sqlite:../Databases/php_auth.sqlite');
$auth = new \Delight\Auth\Auth($db);
@@ -612,6 +614,43 @@ function processRequestData(\Delight\Auth\Auth $auth) {
return 'Username required';
}
}
else if ($_POST['action'] === 'admin.changePasswordForUser') {
if (isset($_POST['newPassword'])) {
if (isset($_POST['id'])) {
try {
$auth->admin()->changePasswordForUserById($_POST['id'], $_POST['newPassword']);
}
catch (\Delight\Auth\UnknownIdException $e) {
return 'unknown ID';
}
catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password';
}
}
elseif (isset($_POST['username'])) {
try {
$auth->admin()->changePasswordForUserByUsername($_POST['username'], $_POST['newPassword']);
}
catch (\Delight\Auth\UnknownUsernameException $e) {
return 'unknown username';
}
catch (\Delight\Auth\AmbiguousUsernameException $e) {
return 'ambiguous username';
}
catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password';
}
}
else {
return 'either ID or username required';
}
}
else {
return 'new password required';
}
return 'ok';
}
else {
throw new Exception('Unexpected action: ' . $_POST['action']);
}
@@ -942,6 +981,20 @@ function showGuestUserForm() {
echo '<input type="text" name="username" placeholder="Username" /> ';
echo '<button type="submit">Log in as user by username</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.changePasswordForUser" />';
echo '<input type="text" name="id" placeholder="ID" /> ';
echo '<input type="text" name="newPassword" placeholder="New password" /> ';
echo '<button type="submit">Change password for user by ID</button>';
echo '</form>';
echo '<form action="" method="post" accept-charset="utf-8">';
echo '<input type="hidden" name="action" value="admin.changePasswordForUser" />';
echo '<input type="text" name="username" placeholder="Username" /> ';
echo '<input type="text" name="newPassword" placeholder="New password" /> ';
echo '<button type="submit">Change password for user by username</button>';
echo '</form>';
}
function showConfirmEmailForm() {