From 0760ea90d60dd4ef0892cc645ec6a6882836b1bd Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 26 Dec 2022 10:22:49 +0000 Subject: [PATCH] use dictionary validator --- app/controllers/api/account.php | 34 ++------ app/controllers/api/users.php | 29 ++----- .../Auth/Validator/PasswordDictionary.php | 78 +++++++++++++++++++ 3 files changed, 89 insertions(+), 52 deletions(-) create mode 100644 src/Appwrite/Auth/Validator/PasswordDictionary.php diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 9f3b82d0f3..dea2ee0ee7 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -41,6 +41,7 @@ use Utopia\Validator\Assoc; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; use Appwrite\Auth\Validator\PasswordHistory; +use Appwrite\Auth\Validator\PasswordDictionary; $oauthDefaultSuccess = '/auth/oauth2/success'; $oauthDefaultFailure = '/auth/oauth2/failure'; @@ -65,16 +66,14 @@ App::post('/v1/account') ->label('abuse-limit', 10) ->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `ID.unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('email', '', new Email(), 'User email.') - ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') + ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary']) ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) - ->inject('passwordsDictionary') ->inject('request') ->inject('response') ->inject('project') ->inject('dbForProject') ->inject('events') - ->action(function (string $userId, string $email, string $password, string $name, string $passwordsDictionary, Request $request, Response $response, Document $project, Database $dbForProject, Event $events) { - + ->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $project, Database $dbForProject, Event $events) { $email = \strtolower($email); if ('console' === $project->getId()) { $whitelistEmails = $project->getAttribute('authWhitelistEmails'); @@ -99,15 +98,6 @@ App::post('/v1/account') } } - $passwordDictionary = $project->getAttribute('auths', [])['passwordDictionary'] ?? false; - if ($passwordDictionary && str_contains($passwordsDictionary, $password)) { - throw new Exception( - Exception::USER_PASSWORD_IN_DICTIONARY, - 'The password is among the common passwords in dictionary.', - 403 - ); - } - $passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; $password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); try { @@ -1520,33 +1510,21 @@ App::patch('/v1/account/password') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_ACCOUNT) - ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.') + ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary']) ->param('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true) - ->inject('passwordsDictionary') ->inject('response') ->inject('user') ->inject('project') ->inject('dbForProject') ->inject('events') - ->action(function (string $password, string $oldPassword, string $passwordsDictionary, Response $response, Document $user, Document $project, Database $dbForProject, Event $events) { + ->action(function (string $password, string $oldPassword, Response $response, Document $user, Document $project, Database $dbForProject, Event $events) { // 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 throw new Exception(Exception::USER_INVALID_CREDENTIALS); } - - + $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); - - $passwordDictionary = $project->getAttribute('auths', [])['passwordDictionary'] ?? false; - if ($passwordDictionary && str_contains($passwordsDictionary, $password)) { - throw new Exception( - Exception::USER_PASSWORD_IN_DICTIONARY, - 'The password is among the common passwords in dictionary.', - 403 - ); - } - $historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; $history = []; if ($historyLimit > 0) { diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 8b04809a96..907e72a6ed 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -35,6 +35,7 @@ use Utopia\Validator\Boolean; use MaxMind\Db\Reader; use Utopia\Validator\Integer; use Appwrite\Auth\Validator\PasswordHistory; +use Appwrite\Auth\Validator\PasswordDictionary; /** 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 $events): Document @@ -105,23 +106,13 @@ App::post('/v1/users') ->param('userId', '', new CustomId(), 'User ID. Choose your own unique ID or pass the string `ID.unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('email', null, new Email(), 'User email.', true) ->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true) - ->param('password', null, new Password(), 'Plain text user password. Must be at least 8 chars.', true) + ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project), 'Plain text user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary']) ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) - ->inject('passwordsDictionary') ->inject('response') ->inject('project') ->inject('dbForProject') ->inject('events') - ->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, string $passwordsDictionary, Response $response, Document $project, Database $dbForProject, Event $events) { - - $passwordDictionary = $project->getAttribute('auths', [])['passwordDictionary'] ?? false; - if ($passwordDictionary && str_contains($passwordsDictionary, $password)) { - throw new Exception( - Exception::USER_PASSWORD_IN_DICTIONARY, - 'The password is among the common passwords in dictionary.', - 403 - ); - } + ->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $events) { $user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $events); @@ -802,13 +793,12 @@ App::patch('/v1/users/:userId/password') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_USER) ->param('userId', '', new UID(), 'User ID.') - ->param('password', '', new Password(), 'New user password. Must be at least 8 chars.') - ->inject('passwordsDictionary') + ->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary']) ->inject('response') ->inject('project') ->inject('dbForProject') ->inject('events') - ->action(function (string $userId, string $password, string $passwordsDictionary, Response $response, Document $project, Database $dbForProject, Event $events) { + ->action(function (string $userId, string $password, Response $response, Document $project, Database $dbForProject, Event $events) { $user = $dbForProject->getDocument('users', $userId); @@ -818,15 +808,6 @@ App::patch('/v1/users/:userId/password') $newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS); - $passwordDictionary = $project->getAttribute('auths', [])['passwordDictionary'] ?? false; - if ($passwordDictionary && str_contains($passwordsDictionary, $password)) { - throw new Exception( - Exception::USER_PASSWORD_IN_DICTIONARY, - 'The password is among the common passwords in dictionary.', - 403 - ); - } - $historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0; $history = []; if ($historyLimit > 0) { diff --git a/src/Appwrite/Auth/Validator/PasswordDictionary.php b/src/Appwrite/Auth/Validator/PasswordDictionary.php new file mode 100644 index 0000000000..b2adf78141 --- /dev/null +++ b/src/Appwrite/Auth/Validator/PasswordDictionary.php @@ -0,0 +1,78 @@ +dictionary = $dictionary; + $this->project = $project; + } + + /** + * Get Description. + * + * Returns validator description + * + * @return string + */ + public function getDescription(): string + { + return 'Password must be at least 8 characters and should not be one of the commonly used password.'; + } + + /** + * Is valid. + * + * @param mixed $value + * + * @return bool + */ + public function isValid($value): bool + { + if(!parent::isValid($value)) { + return false; + } + + $dictionaryEnabled = $this->project->getAttribute('auths', [])['passwordDictionary'] ?? false; + if ($dictionaryEnabled && array_key_exists($value, $this->dictionary)) { + return false; + } + return true; + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_STRING; + } +}