Merge pull request #4906 from appwrite/feat-password-dictionary
password dictionary
This commit is contained in:
commit
cf0b039653
11 changed files with 10310 additions and 23 deletions
|
@ -1,6 +1,7 @@
|
||||||
## Version 1.3.0
|
## Version 1.3.0
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
- Password dictionary setting allows to compare user's password against command password database [4906](https://github.com/appwrite/appwrite/pull/4906)
|
||||||
- Password history setting allows to save user's last used password so that it may not be used again. Maximum number of history saved is 20, which can be configured. Minimum is 0 which means disabled. [#4866](https://github.com/appwrite/appwrite/pull/4866)
|
- Password history setting allows to save user's last used password so that it may not be used again. Maximum number of history saved is 20, which can be configured. Minimum is 0 which means disabled. [#4866](https://github.com/appwrite/appwrite/pull/4866)
|
||||||
|
|
||||||
# Version 1.2.0
|
# Version 1.2.0
|
||||||
|
|
10000
app/assets/security/10k-common-passwords
Normal file
10000
app/assets/security/10k-common-passwords
Normal file
File diff suppressed because it is too large
Load diff
|
@ -41,6 +41,7 @@ use Utopia\Validator\Assoc;
|
||||||
use Utopia\Validator\Text;
|
use Utopia\Validator\Text;
|
||||||
use Utopia\Validator\WhiteList;
|
use Utopia\Validator\WhiteList;
|
||||||
use Appwrite\Auth\Validator\PasswordHistory;
|
use Appwrite\Auth\Validator\PasswordHistory;
|
||||||
|
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||||
|
|
||||||
$oauthDefaultSuccess = '/auth/oauth2/success';
|
$oauthDefaultSuccess = '/auth/oauth2/success';
|
||||||
$oauthDefaultFailure = '/auth/oauth2/failure';
|
$oauthDefaultFailure = '/auth/oauth2/failure';
|
||||||
|
@ -65,7 +66,7 @@ App::post('/v1/account')
|
||||||
->label('abuse-limit', 10)
|
->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('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('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->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
|
||||||
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
|
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
|
||||||
->inject('request')
|
->inject('request')
|
||||||
->inject('response')
|
->inject('response')
|
||||||
|
@ -73,7 +74,6 @@ App::post('/v1/account')
|
||||||
->inject('dbForProject')
|
->inject('dbForProject')
|
||||||
->inject('events')
|
->inject('events')
|
||||||
->action(function (string $userId, string $email, string $password, string $name, 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);
|
$email = \strtolower($email);
|
||||||
if ('console' === $project->getId()) {
|
if ('console' === $project->getId()) {
|
||||||
$whitelistEmails = $project->getAttribute('authWhitelistEmails');
|
$whitelistEmails = $project->getAttribute('authWhitelistEmails');
|
||||||
|
@ -99,7 +99,6 @@ App::post('/v1/account')
|
||||||
}
|
}
|
||||||
|
|
||||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||||
|
|
||||||
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
$password = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||||
try {
|
try {
|
||||||
$userId = $userId == 'unique()' ? ID::unique() : $userId;
|
$userId = $userId == 'unique()' ? ID::unique() : $userId;
|
||||||
|
@ -113,11 +112,11 @@ App::post('/v1/account')
|
||||||
'email' => $email,
|
'email' => $email,
|
||||||
'emailVerification' => false,
|
'emailVerification' => false,
|
||||||
'status' => true,
|
'status' => true,
|
||||||
'passwordHistory' => $passwordHistory > 0 ? [$password] : [],
|
|
||||||
'password' => $password,
|
'password' => $password,
|
||||||
|
'passwordHistory' => $passwordHistory > 0 ? [$password] : [],
|
||||||
|
'passwordUpdate' => DateTime::now(),
|
||||||
'hash' => Auth::DEFAULT_ALGO,
|
'hash' => Auth::DEFAULT_ALGO,
|
||||||
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
|
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
|
||||||
'passwordUpdate' => DateTime::now(),
|
|
||||||
'registration' => DateTime::now(),
|
'registration' => DateTime::now(),
|
||||||
'reset' => false,
|
'reset' => false,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
|
@ -1534,7 +1533,7 @@ App::patch('/v1/account/password')
|
||||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||||
->label('sdk.response.model', Response::MODEL_ACCOUNT)
|
->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->getAttribute('auths', [])['passwordDictionary'] ?? false), '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)
|
->param('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true)
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('user')
|
->inject('user')
|
||||||
|
@ -1549,7 +1548,6 @@ App::patch('/v1/account/password')
|
||||||
}
|
}
|
||||||
|
|
||||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||||
|
|
||||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||||
$history = [];
|
$history = [];
|
||||||
if ($historyLimit > 0) {
|
if ($historyLimit > 0) {
|
||||||
|
@ -1564,11 +1562,11 @@ App::patch('/v1/account/password')
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user
|
$user = $dbForProject->updateDocument('users', $user->getId(), $user
|
||||||
->setAttribute('passwordHistory', $history)
|
|
||||||
->setAttribute('password', $newPassword)
|
->setAttribute('password', $newPassword)
|
||||||
|
->setAttribute('passwordHistory', $history)
|
||||||
|
->setAttribute('passwordUpdate', DateTime::now()))
|
||||||
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
||||||
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
|
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
|
||||||
->setAttribute('passwordUpdate', DateTime::now()));
|
|
||||||
|
|
||||||
$events->setParam('userId', $user->getId());
|
$events->setParam('userId', $user->getId());
|
||||||
|
|
||||||
|
@ -2136,9 +2134,9 @@ App::put('/v1/account/recovery')
|
||||||
|
|
||||||
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
|
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
|
||||||
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
|
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
|
||||||
|
->setAttribute('passwordUpdate', DateTime::now())
|
||||||
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
||||||
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
|
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
|
||||||
->setAttribute('passwordUpdate', DateTime::now())
|
|
||||||
->setAttribute('emailVerification', true));
|
->setAttribute('emailVerification', true));
|
||||||
|
|
||||||
$recoveryDocument = $dbForProject->getDocument('tokens', $recovery);
|
$recoveryDocument = $dbForProject->getDocument('tokens', $recovery);
|
||||||
|
|
|
@ -81,7 +81,7 @@ App::post('/v1/projects')
|
||||||
}
|
}
|
||||||
|
|
||||||
$auth = Config::getParam('auth', []);
|
$auth = Config::getParam('auth', []);
|
||||||
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG];
|
$auths = ['limit' => 0, 'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT, 'passwordHistory' => 0, 'passwordDictionary' => false, 'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG];
|
||||||
foreach ($auth as $index => $method) {
|
foreach ($auth as $index => $method) {
|
||||||
$auths[$method['key'] ?? ''] = true;
|
$auths[$method['key'] ?? ''] = true;
|
||||||
}
|
}
|
||||||
|
@ -577,12 +577,12 @@ App::patch('/v1/projects/:projectId/auth/:method')
|
||||||
});
|
});
|
||||||
|
|
||||||
App::patch('/v1/projects/:projectId/auth/password-history')
|
App::patch('/v1/projects/:projectId/auth/password-history')
|
||||||
->desc('Update Project users limit')
|
->desc('Update authentication password history. Use this endpoint to set the number of password history to save and 0 to disable password history.')
|
||||||
->groups(['api', 'projects'])
|
->groups(['api', 'projects'])
|
||||||
->label('scope', 'projects.write')
|
->label('scope', 'projects.write')
|
||||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||||
->label('sdk.namespace', 'projects')
|
->label('sdk.namespace', 'projects')
|
||||||
->label('sdk.method', 'updateAuthLimit')
|
->label('sdk.method', 'updateAuthPasswordHistory')
|
||||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||||
->label('sdk.response.model', Response::MODEL_PROJECT)
|
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||||
|
@ -607,6 +607,37 @@ App::patch('/v1/projects/:projectId/auth/password-history')
|
||||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
App::patch('/v1/projects/:projectId/auth/password-dictionary')
|
||||||
|
->desc('Update authentication password disctionary status. Use this endpoint to enable or disable the dicitonary check for user password')
|
||||||
|
->groups(['api', 'projects'])
|
||||||
|
->label('scope', 'projects.write')
|
||||||
|
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||||
|
->label('sdk.namespace', 'projects')
|
||||||
|
->label('sdk.method', 'updateAuthPasswordDictionary')
|
||||||
|
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||||
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||||
|
->label('sdk.response.model', Response::MODEL_PROJECT)
|
||||||
|
->param('projectId', '', new UID(), 'Project unique ID.')
|
||||||
|
->param('enabled', false, new Boolean(false), 'Set whether or not to enable checking user\'s password against most commonly used passwords. Default is false.')
|
||||||
|
->inject('response')
|
||||||
|
->inject('dbForConsole')
|
||||||
|
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForConsole) {
|
||||||
|
|
||||||
|
$project = $dbForConsole->getDocument('projects', $projectId);
|
||||||
|
|
||||||
|
if ($project->isEmpty()) {
|
||||||
|
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$auths = $project->getAttribute('auths', []);
|
||||||
|
$auths['passwordDictionary'] = $enabled;
|
||||||
|
|
||||||
|
$dbForConsole->updateDocument('projects', $project->getId(), $project
|
||||||
|
->setAttribute('auths', $auths));
|
||||||
|
|
||||||
|
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||||
|
});
|
||||||
|
|
||||||
App::patch('/v1/projects/:projectId/auth/max-sessions')
|
App::patch('/v1/projects/:projectId/auth/max-sessions')
|
||||||
->desc('Update Project user sessions limit')
|
->desc('Update Project user sessions limit')
|
||||||
->groups(['api', 'projects'])
|
->groups(['api', 'projects'])
|
||||||
|
|
|
@ -35,6 +35,7 @@ use Utopia\Validator\Boolean;
|
||||||
use MaxMind\Db\Reader;
|
use MaxMind\Db\Reader;
|
||||||
use Utopia\Validator\Integer;
|
use Utopia\Validator\Integer;
|
||||||
use Appwrite\Auth\Validator\PasswordHistory;
|
use Appwrite\Auth\Validator\PasswordHistory;
|
||||||
|
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||||
|
|
||||||
/** TODO: Remove function when we move to using utopia/platform */
|
/** 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
|
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Event $events): Document
|
||||||
|
@ -64,11 +65,11 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
|
||||||
'phone' => $phone,
|
'phone' => $phone,
|
||||||
'phoneVerification' => false,
|
'phoneVerification' => false,
|
||||||
'status' => true,
|
'status' => true,
|
||||||
'passwordHistory' => is_null($password) && $passwordHistory === 0 ? [] : [$password],
|
|
||||||
'password' => $password,
|
'password' => $password,
|
||||||
|
'passwordHistory' => is_null($password) && $passwordHistory === 0 ? [] : [$password],
|
||||||
|
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
|
||||||
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
|
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
|
||||||
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptionsObject + ['type' => $hash],
|
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptionsObject + ['type' => $hash],
|
||||||
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
|
|
||||||
'registration' => DateTime::now(),
|
'registration' => DateTime::now(),
|
||||||
'reset' => false,
|
'reset' => false,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
|
@ -105,13 +106,14 @@ 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('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('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('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->getAttribute('auths', [])['passwordDictionary'] ?? false), 'Plain text user password. Must be at least 8 chars.', true, ['project', 'passwordsDictionary'])
|
||||||
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
|
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('project')
|
->inject('project')
|
||||||
->inject('dbForProject')
|
->inject('dbForProject')
|
||||||
->inject('events')
|
->inject('events')
|
||||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Event $events) {
|
->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);
|
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $events);
|
||||||
|
|
||||||
$response
|
$response
|
||||||
|
@ -791,7 +793,7 @@ App::patch('/v1/users/:userId/password')
|
||||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||||
->label('sdk.response.model', Response::MODEL_USER)
|
->label('sdk.response.model', Response::MODEL_USER)
|
||||||
->param('userId', '', new UID(), 'User ID.')
|
->param('userId', '', new UID(), 'User ID.')
|
||||||
->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
|
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('project')
|
->inject('project')
|
||||||
->inject('dbForProject')
|
->inject('dbForProject')
|
||||||
|
@ -820,11 +822,11 @@ App::patch('/v1/users/:userId/password')
|
||||||
}
|
}
|
||||||
|
|
||||||
$user
|
$user
|
||||||
->setAttribute('passwordHistory', $history)
|
|
||||||
->setAttribute('password', $newPassword)
|
->setAttribute('password', $newPassword)
|
||||||
|
->setAttribute('passwordHistory', $history)
|
||||||
|
->setAttribute('passwordUpdate', DateTime::now())
|
||||||
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
||||||
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
|
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
|
||||||
->setAttribute('passwordUpdate', DateTime::now());
|
|
||||||
|
|
||||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||||
|
|
||||||
|
|
11
app/init.php
11
app/init.php
|
@ -607,6 +607,12 @@ $register->set('smtp', function () {
|
||||||
$register->set('geodb', function () {
|
$register->set('geodb', function () {
|
||||||
return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2022-06.mmdb');
|
return new Reader(__DIR__ . '/assets/dbip/dbip-country-lite-2022-06.mmdb');
|
||||||
});
|
});
|
||||||
|
$register->set('passwordsDictionary', function () {
|
||||||
|
$content = \file_get_contents(__DIR__ . '/assets/security/10k-common-passwords');
|
||||||
|
$content = explode("\n", $content);
|
||||||
|
$content = array_flip($content);
|
||||||
|
return $content;
|
||||||
|
});
|
||||||
$register->set('db', function () {
|
$register->set('db', function () {
|
||||||
// This is usually for our workers or CLI commands scope
|
// This is usually for our workers or CLI commands scope
|
||||||
$dbHost = App::getEnv('_APP_DB_HOST', '');
|
$dbHost = App::getEnv('_APP_DB_HOST', '');
|
||||||
|
@ -1027,6 +1033,11 @@ App::setResource('geodb', function ($register) {
|
||||||
return $register->get('geodb');
|
return $register->get('geodb');
|
||||||
}, ['register']);
|
}, ['register']);
|
||||||
|
|
||||||
|
App::setResource('passwordsDictionary', function ($register) {
|
||||||
|
/** @var Utopia\Registry\Registry $register */
|
||||||
|
return $register->get('passwordsDictionary');
|
||||||
|
}, ['register']);
|
||||||
|
|
||||||
App::setResource('sms', function () {
|
App::setResource('sms', function () {
|
||||||
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
|
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
|
||||||
$user = $dsn->getUser();
|
$user = $dsn->getUser();
|
||||||
|
|
77
src/Appwrite/Auth/Validator/PasswordDictionary.php
Normal file
77
src/Appwrite/Auth/Validator/PasswordDictionary.php
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Appwrite\Auth\Validator;
|
||||||
|
|
||||||
|
use Utopia\Database\Document;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password.
|
||||||
|
*
|
||||||
|
* Validates user password string
|
||||||
|
*/
|
||||||
|
class PasswordDictionary extends Password
|
||||||
|
{
|
||||||
|
protected array $dictionary;
|
||||||
|
protected bool $enabled;
|
||||||
|
|
||||||
|
public function __construct(array $dictionary, bool $enabled = false)
|
||||||
|
{
|
||||||
|
$this->dictionary = $dictionary;
|
||||||
|
$this->enabled = $enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->enabled && 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,7 +82,7 @@ class V18 extends Migration
|
||||||
*/
|
*/
|
||||||
$document->setAttribute('version', '1.2.0');
|
$document->setAttribute('version', '1.2.0');
|
||||||
break;
|
break;
|
||||||
case 'users':
|
case 'projects':
|
||||||
/**
|
/**
|
||||||
* Bump version number.
|
* Bump version number.
|
||||||
*/
|
*/
|
||||||
|
@ -92,7 +92,8 @@ class V18 extends Migration
|
||||||
* Set default passwordHistory
|
* Set default passwordHistory
|
||||||
*/
|
*/
|
||||||
$document->setAttribute('auths', array_merge($document->getAttribute('auths', []), [
|
$document->setAttribute('auths', array_merge($document->getAttribute('auths', []), [
|
||||||
'passwordHistory' => 0
|
'passwordHistory' => 0,
|
||||||
|
'passwordDictionary' => false,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -126,6 +126,12 @@ class Project extends Model
|
||||||
'default' => 0,
|
'default' => 0,
|
||||||
'example' => 5,
|
'example' => 5,
|
||||||
])
|
])
|
||||||
|
->addRule('authPasswordDictionary', [
|
||||||
|
'type' => self::TYPE_BOOLEAN,
|
||||||
|
'description' => 'Whether or not to check user\'s password against most commonly used passwords.',
|
||||||
|
'default' => false,
|
||||||
|
'example' => true,
|
||||||
|
])
|
||||||
->addRule('providers', [
|
->addRule('providers', [
|
||||||
'type' => Response::MODEL_PROVIDER,
|
'type' => Response::MODEL_PROVIDER,
|
||||||
'description' => 'List of Providers.',
|
'description' => 'List of Providers.',
|
||||||
|
@ -247,6 +253,7 @@ class Project extends Model
|
||||||
$document->setAttribute('authDuration', $authValues['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG);
|
$document->setAttribute('authDuration', $authValues['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG);
|
||||||
$document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT);
|
$document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT);
|
||||||
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
|
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
|
||||||
|
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
|
||||||
|
|
||||||
foreach ($auth as $index => $method) {
|
foreach ($auth as $index => $method) {
|
||||||
$key = $method['key'];
|
$key = $method['key'];
|
||||||
|
|
|
@ -1101,6 +1101,137 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @depends testUpdateProjectAuthLimit
|
||||||
|
*/
|
||||||
|
public function testUpdateProjectAuthPasswordDictionary($data): array
|
||||||
|
{
|
||||||
|
$id = $data['projectId'] ?? '';
|
||||||
|
|
||||||
|
$password = 'password';
|
||||||
|
$name = 'User Name';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for Success
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create account
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $id,
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => uniqid() . 'user@localhost.test',
|
||||||
|
'password' => $password,
|
||||||
|
'name' => $name,
|
||||||
|
]);
|
||||||
|
$this->assertEquals(201, $response['headers']['status-code']);
|
||||||
|
$userId = $response['body']['$id'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create user
|
||||||
|
*/
|
||||||
|
$user = $this->client->call(Client::METHOD_POST, '/users', array_merge($this->getHeaders(), [
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $id,
|
||||||
|
'x-appwrite-mode' => 'admin',
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => uniqid() . 'user@localhost.test',
|
||||||
|
'password' => 'password',
|
||||||
|
'name' => 'Cristiano Ronaldo',
|
||||||
|
]);
|
||||||
|
$this->assertEquals(201, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable Disctionary
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-dictionary', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
], $this->getHeaders()), [
|
||||||
|
'enabled' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
$this->assertEquals(true, $response['body']['authPasswordDictionary']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for failure
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $id,
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => uniqid() . 'user@localhost.test',
|
||||||
|
'password' => $password,
|
||||||
|
'name' => $name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(400, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create user
|
||||||
|
*/
|
||||||
|
$user = $this->client->call(Client::METHOD_POST, '/users', array_merge($this->getHeaders(), [
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $id,
|
||||||
|
'x-appwrite-mode' => 'admin',
|
||||||
|
]), [
|
||||||
|
'userId' => ID::unique(),
|
||||||
|
'email' => uniqid() . 'user@localhost.test',
|
||||||
|
'password' => 'password',
|
||||||
|
'name' => 'Cristiano Ronaldo',
|
||||||
|
]);
|
||||||
|
$this->assertEquals(400, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
$headers = array_merge($this->getHeaders(), [
|
||||||
|
'x-appwrite-mode' => 'admin',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_PATCH, '/users/' . $userId . '/password', $headers, [
|
||||||
|
'password' => $password,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(400, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-history', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
], $this->getHeaders()), [
|
||||||
|
'limit' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
$this->assertEquals(0, $response['body']['authPasswordHistory']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset
|
||||||
|
*/
|
||||||
|
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-dictionary', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
], $this->getHeaders()), [
|
||||||
|
'enabled' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
$this->assertEquals(false, $response['body']['authPasswordDictionary']);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function testUpdateProjectServiceStatusAdmin(): array
|
public function testUpdateProjectServiceStatusAdmin(): array
|
||||||
{
|
{
|
||||||
|
|
28
tests/unit/Auth/Validator/PasswordDictionaryTest.php
Normal file
28
tests/unit/Auth/Validator/PasswordDictionaryTest.php
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Auth\Validator;
|
||||||
|
|
||||||
|
use Appwrite\Auth\Validator\PasswordDictionary;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Utopia\Database\Document;
|
||||||
|
|
||||||
|
class PasswordDictionaryTest extends TestCase
|
||||||
|
{
|
||||||
|
protected ?PasswordDictionary $object = null;
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->object = new PasswordDictionary(
|
||||||
|
['password' => true, '123456' => true],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValues(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals($this->object->isValid('1'), false); // to check parent is being called
|
||||||
|
$this->assertEquals($this->object->isValid('123456'), false);
|
||||||
|
$this->assertEquals($this->object->isValid('password'), false);
|
||||||
|
$this->assertEquals($this->object->isValid('myPasswordIsRight'), true);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue