From ba32170f72ea120cf424716d4e3c9df5e68b7364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 4 Jan 2024 16:26:15 +0100 Subject: [PATCH] Improve password validator hook --- app/controllers/api/account.php | 5 ++- app/controllers/api/users.php | 58 +++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 61e129e935..70b77bf3c0 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1887,7 +1887,8 @@ App::patch('/v1/account/password') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) { + ->inject('hooks') + ->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { // Check old password only if its an existing user. if (!empty($user->getAttribute('passwordUpdate')) && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password @@ -1914,6 +1915,8 @@ App::patch('/v1/account/password') } } + $hooks->trigger('passwordValidator', [$project, $password, $user]); + $user ->setAttribute('password', $newPassword) ->setAttribute('passwordHistory', $history) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index c0efd9633a..91eb47822d 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -39,9 +39,10 @@ use Utopia\Validator\Integer; use Appwrite\Auth\Validator\PasswordHistory; use Appwrite\Auth\Validator\PasswordDictionary; use Appwrite\Auth\Validator\PersonalData; +use Appwrite\Hooks\Hooks; /** TODO: Remove function when we move to using utopia/platform */ -function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents): Document +function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks): Document { $hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array $passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; @@ -70,8 +71,7 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e } } - $password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null; - $user = $dbForProject->createDocument('users', new Document([ + $user = new Document([ '$id' => $userId, '$permissions' => [ Permission::read(Role::any()), @@ -97,7 +97,12 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $phone, $name]), - ])); + ]); + + $hooks->trigger('passwordValidator', [$project, $password, $user]); + + $password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null; + $user = $dbForProject->createDocument('users', $user); } catch (Duplicate $th) { throw new Exception(Exception::USER_ALREADY_EXISTS); } @@ -130,8 +135,9 @@ App::post('/v1/users') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) { - $user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $queueForEvents); + ->inject('hooks') + ->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { + $user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $queueForEvents, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -160,8 +166,9 @@ App::post('/v1/users/bcrypt') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) { - $user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents); + ->inject('hooks') + ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { + $user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -190,8 +197,9 @@ App::post('/v1/users/md5') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) { - $user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents); + ->inject('hooks') + ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { + $user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -220,8 +228,9 @@ App::post('/v1/users/argon2') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) { - $user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents); + ->inject('hooks') + ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { + $user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -251,14 +260,15 @@ App::post('/v1/users/sha') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) { + ->inject('hooks') + ->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { $options = '{}'; if (!empty($passwordVersion)) { $options = '{"version":"' . $passwordVersion . '"}'; } - $user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents); + $user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -287,8 +297,9 @@ App::post('/v1/users/phpass') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) { - $user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents); + ->inject('hooks') + ->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { + $user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -322,7 +333,8 @@ App::post('/v1/users/scrypt') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) { + ->inject('hooks') + ->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { $options = [ 'salt' => $passwordSalt, 'costCpu' => $passwordCpu, @@ -331,7 +343,7 @@ App::post('/v1/users/scrypt') 'length' => $passwordLength ]; - $user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents); + $user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -363,8 +375,9 @@ App::post('/v1/users/scrypt-modified') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) { - $user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents); + ->inject('hooks') + ->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { + $user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $queueForEvents, $hooks); $response ->setStatusCode(Response::STATUS_CODE_CREATED) @@ -846,7 +859,8 @@ App::patch('/v1/users/:userId/password') ->inject('project') ->inject('dbForProject') ->inject('queueForEvents') - ->action(function (string $userId, string $password, Response $response, Document $project, Database $dbForProject, Event $queueForEvents) { + ->inject('hooks') + ->action(function (string $userId, string $password, Response $response, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { $user = $dbForProject->getDocument('users', $userId); @@ -861,6 +875,8 @@ App::patch('/v1/users/:userId/password') } } + $hooks->trigger('passwordValidator', [$project, $password, $user]); + $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); $historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;