mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Add project password policy settings
This commit is contained in:
@@ -394,7 +394,7 @@ Http::post('/v1/account')
|
||||
->label('abuse-limit', 10)
|
||||
->param('userId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. 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.', false, ['dbForProject'])
|
||||
->param('email', '', new EmailValidator(), 'User email.')
|
||||
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
|
||||
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false, false, $project->getAttribute('auths', [])['passwordPolicy'] ?? []), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
|
||||
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
@@ -3378,7 +3378,7 @@ Http::patch('/v1/account/password')
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->label('abuse-limit', 10)
|
||||
->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('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false, false, $project->getAttribute('auths', [])['passwordPolicy'] ?? []), '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('response')
|
||||
->inject('user')
|
||||
@@ -3988,7 +3988,7 @@ Http::put('/v1/account/recovery')
|
||||
->label('abuse-key', 'url:{url},userId:{param-userId}')
|
||||
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
|
||||
->param('secret', '', new Text(256), 'Valid reset token.')
|
||||
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
|
||||
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false, false, $project->getAttribute('auths', [])['passwordPolicy'] ?? []), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('dbForProject')
|
||||
|
||||
@@ -555,6 +555,206 @@ Http::patch('/v1/projects/:projectId/auth/password-history')
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/auth/password-policy/min-length')
|
||||
->desc('Update the minimum password length requirement.')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'auth',
|
||||
name: 'updateAuthPasswordPolicyMinLength',
|
||||
description: '/docs/references/projects/update-auth-password-policy-min-length.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('minLength', 8, new Range(8, 256), 'Set the minimum password length. Value must be between 8 and 256. Default is 8.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, int $minLength, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['passwordPolicy'] = \array_merge($auths['passwordPolicy'] ?? [], [
|
||||
'minLength' => $minLength,
|
||||
]);
|
||||
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('auths', $auths));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/auth/password-policy/uppercase')
|
||||
->desc('Update the uppercase password requirement.')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'auth',
|
||||
name: 'updateAuthPasswordPolicyUppercase',
|
||||
description: '/docs/references/projects/update-auth-password-policy-uppercase.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('enabled', false, new Boolean(false), 'Set whether or not passwords must include at least one uppercase letter. Default is false.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['passwordPolicy'] = \array_merge($auths['passwordPolicy'] ?? [], [
|
||||
'requireUppercase' => $enabled,
|
||||
]);
|
||||
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('auths', $auths));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/auth/password-policy/lowercase')
|
||||
->desc('Update the lowercase password requirement.')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'auth',
|
||||
name: 'updateAuthPasswordPolicyLowercase',
|
||||
description: '/docs/references/projects/update-auth-password-policy-lowercase.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('enabled', false, new Boolean(false), 'Set whether or not passwords must include at least one lowercase letter. Default is false.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['passwordPolicy'] = \array_merge($auths['passwordPolicy'] ?? [], [
|
||||
'requireLowercase' => $enabled,
|
||||
]);
|
||||
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('auths', $auths));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/auth/password-policy/number')
|
||||
->desc('Update the numeric password requirement.')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'auth',
|
||||
name: 'updateAuthPasswordPolicyNumber',
|
||||
description: '/docs/references/projects/update-auth-password-policy-number.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('enabled', false, new Boolean(false), 'Set whether or not passwords must include at least one number. Default is false.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['passwordPolicy'] = \array_merge($auths['passwordPolicy'] ?? [], [
|
||||
'requireNumber' => $enabled,
|
||||
]);
|
||||
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('auths', $auths));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/auth/password-policy/special-char')
|
||||
->desc('Update the special character password requirement.')
|
||||
->groups(['api', 'projects'])
|
||||
->label('scope', 'projects.write')
|
||||
->label('sdk', new Method(
|
||||
namespace: 'projects',
|
||||
group: 'auth',
|
||||
name: 'updateAuthPasswordPolicySpecialChar',
|
||||
description: '/docs/references/projects/update-auth-password-policy-special-char.md',
|
||||
auth: [AuthType::ADMIN],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_PROJECT,
|
||||
)
|
||||
]
|
||||
))
|
||||
->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform'])
|
||||
->param('enabled', false, new Boolean(false), 'Set whether or not passwords must include at least one special character. Default is false.')
|
||||
->inject('response')
|
||||
->inject('dbForPlatform')
|
||||
->action(function (string $projectId, bool $enabled, Response $response, Database $dbForPlatform) {
|
||||
|
||||
$project = $dbForPlatform->getDocument('projects', $projectId);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
$auths = $project->getAttribute('auths', []);
|
||||
$auths['passwordPolicy'] = \array_merge($auths['passwordPolicy'] ?? [], [
|
||||
'requireSpecialChar' => $enabled,
|
||||
]);
|
||||
|
||||
$dbForPlatform->updateDocument('projects', $project->getId(), $project
|
||||
->setAttribute('auths', $auths));
|
||||
|
||||
$response->dynamic($project, Response::MODEL_PROJECT);
|
||||
});
|
||||
|
||||
Http::patch('/v1/projects/:projectId/auth/password-dictionary')
|
||||
->desc('Update authentication password dictionary status. Use this endpoint to enable or disable the dicitonary check for user password')
|
||||
->groups(['api', 'projects'])
|
||||
|
||||
@@ -278,7 +278,7 @@ Http::post('/v1/users')
|
||||
->param('userId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. 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.', false, ['dbForProject'])
|
||||
->param('email', null, new Nullable(new EmailValidator()), 'User email.', true)
|
||||
->param('phone', null, new Nullable(new Phone()), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', 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('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false, false, $project->getAttribute('auths', [])['passwordPolicy'] ?? []), '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)
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
@@ -1395,7 +1395,7 @@ Http::patch('/v1/users/:userId/password')
|
||||
]
|
||||
))
|
||||
->param('userId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'User ID.', false, ['dbForProject'])
|
||||
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, enabled: $project->getAttribute('auths', [])['passwordDictionary'] ?? false, allowEmpty: true), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
|
||||
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, enabled: $project->getAttribute('auths', [])['passwordDictionary'] ?? false, allowEmpty: true, policy: $project->getAttribute('auths', [])['passwordPolicy'] ?? []), 'New user password. Must be at least 8 chars.', false, ['project', 'passwordsDictionary'])
|
||||
->inject('response')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Update whether authentication passwords must include at least one lowercase letter.
|
||||
@@ -0,0 +1 @@
|
||||
Update the minimum authentication password length requirement.
|
||||
@@ -0,0 +1 @@
|
||||
Update whether authentication passwords must include at least one number.
|
||||
@@ -0,0 +1 @@
|
||||
Update whether authentication passwords must include at least one special character.
|
||||
@@ -0,0 +1 @@
|
||||
Update whether authentication passwords must include at least one uppercase letter.
|
||||
@@ -7,14 +7,14 @@ namespace Appwrite\Auth\Validator;
|
||||
*
|
||||
* Validates user password string
|
||||
*/
|
||||
class PasswordDictionary extends Password
|
||||
class PasswordDictionary extends PasswordPolicy
|
||||
{
|
||||
protected array $dictionary;
|
||||
protected bool $enabled;
|
||||
|
||||
public function __construct(array $dictionary, bool $enabled = false, bool $allowEmpty = false)
|
||||
public function __construct(array $dictionary, bool $enabled = false, bool $allowEmpty = false, array $policy = [])
|
||||
{
|
||||
parent::__construct($allowEmpty);
|
||||
parent::__construct($policy, $allowEmpty);
|
||||
$this->dictionary = $dictionary;
|
||||
$this->enabled = $enabled;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class PasswordDictionary extends Password
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Password must be between 8 and 265 characters long, and should not be one of the commonly used password.';
|
||||
return parent::getDescription() . ' It should not be one of the commonly used passwords.';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Auth\Validator;
|
||||
|
||||
/**
|
||||
* PasswordPolicy.
|
||||
*
|
||||
* Validates password complexity rules.
|
||||
*/
|
||||
class PasswordPolicy extends Password
|
||||
{
|
||||
protected int $minLength;
|
||||
protected bool $requireUppercase;
|
||||
protected bool $requireLowercase;
|
||||
protected bool $requireNumber;
|
||||
protected bool $requireSpecialChar;
|
||||
|
||||
public function __construct(array $policy = [], bool $allowEmpty = false)
|
||||
{
|
||||
parent::__construct($allowEmpty);
|
||||
|
||||
$this->minLength = $policy['minLength'] ?? 8;
|
||||
$this->requireUppercase = $policy['requireUppercase'] ?? false;
|
||||
$this->requireLowercase = $policy['requireLowercase'] ?? false;
|
||||
$this->requireNumber = $policy['requireNumber'] ?? false;
|
||||
$this->requireSpecialChar = $policy['requireSpecialChar'] ?? false;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
$requirements = [
|
||||
'between ' . $this->minLength . ' and 256 characters long',
|
||||
];
|
||||
|
||||
if ($this->requireUppercase) {
|
||||
$requirements[] = 'include an uppercase letter';
|
||||
}
|
||||
|
||||
if ($this->requireLowercase) {
|
||||
$requirements[] = 'include a lowercase letter';
|
||||
}
|
||||
|
||||
if ($this->requireNumber) {
|
||||
$requirements[] = 'include a number';
|
||||
}
|
||||
|
||||
if ($this->requireSpecialChar) {
|
||||
$requirements[] = 'include a special character';
|
||||
}
|
||||
|
||||
return 'Password must be ' . \implode(', ', $requirements) . '.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
if (!parent::isValid($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->allowEmpty && \strlen($value) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (\strlen($value) < $this->minLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->requireUppercase && !\preg_match('/[A-Z]/', $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->requireLowercase && !\preg_match('/[a-z]/', $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->requireNumber && !\preg_match('/\d/', $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->requireSpecialChar && !\preg_match("/[!\"#$%&'()*+,\-.\/:;<=>?@[\\\\\]^_`{|}~]/", $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,13 @@ class Create extends Action
|
||||
$auths = [
|
||||
'limit' => 0,
|
||||
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
|
||||
'passwordPolicy' => [
|
||||
'minLength' => 8,
|
||||
'requireUppercase' => false,
|
||||
'requireLowercase' => false,
|
||||
'requireNumber' => false,
|
||||
'requireSpecialChar' => false,
|
||||
],
|
||||
'passwordHistory' => 0,
|
||||
'passwordDictionary' => false,
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
|
||||
@@ -120,6 +120,36 @@ class Project extends Model
|
||||
'default' => 0,
|
||||
'example' => 5,
|
||||
])
|
||||
->addRule('authPasswordPolicyMinLength', [
|
||||
'type' => self::TYPE_INTEGER,
|
||||
'description' => 'Minimum password length required for user passwords.',
|
||||
'default' => 8,
|
||||
'example' => 12,
|
||||
])
|
||||
->addRule('authPasswordPolicyRequireUppercase', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not passwords must include at least one uppercase letter.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authPasswordPolicyRequireLowercase', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not passwords must include at least one lowercase letter.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authPasswordPolicyRequireNumber', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not passwords must include at least one number.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authPasswordPolicyRequireSpecialChar', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not passwords must include at least one special character.',
|
||||
'default' => false,
|
||||
'example' => true,
|
||||
])
|
||||
->addRule('authPasswordDictionary', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Whether or not to check user\'s password against most commonly used passwords.',
|
||||
@@ -426,12 +456,18 @@ class Project extends Model
|
||||
}
|
||||
|
||||
$authValues = $document->getAttribute('auths', []);
|
||||
$passwordPolicy = $authValues['passwordPolicy'] ?? [];
|
||||
$auth = Config::getParam('auth', []);
|
||||
|
||||
$document->setAttribute('authLimit', $authValues['limit'] ?? 0);
|
||||
$document->setAttribute('authDuration', $authValues['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG);
|
||||
$document->setAttribute('authSessionsLimit', $authValues['maxSessions'] ?? APP_LIMIT_USER_SESSIONS_DEFAULT);
|
||||
$document->setAttribute('authPasswordHistory', $authValues['passwordHistory'] ?? 0);
|
||||
$document->setAttribute('authPasswordPolicyMinLength', $passwordPolicy['minLength'] ?? 8);
|
||||
$document->setAttribute('authPasswordPolicyRequireUppercase', $passwordPolicy['requireUppercase'] ?? false);
|
||||
$document->setAttribute('authPasswordPolicyRequireLowercase', $passwordPolicy['requireLowercase'] ?? false);
|
||||
$document->setAttribute('authPasswordPolicyRequireNumber', $passwordPolicy['requireNumber'] ?? false);
|
||||
$document->setAttribute('authPasswordPolicyRequireSpecialChar', $passwordPolicy['requireSpecialChar'] ?? false);
|
||||
$document->setAttribute('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
|
||||
$document->setAttribute('authPersonalDataCheck', $authValues['personalDataCheck'] ?? false);
|
||||
$document->setAttribute('authDisposableEmails', $authValues['disposableEmails'] ?? false);
|
||||
|
||||
@@ -2006,6 +2006,229 @@ class ProjectsConsoleClientTest extends Scope
|
||||
$this->assertEquals(0, $response['body']['authPasswordHistory']);
|
||||
}
|
||||
|
||||
public function testUpdateProjectAuthPasswordPolicy(): void
|
||||
{
|
||||
$data = $this->setupProjectData();
|
||||
$id = $data['projectId'];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/min-length', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'minLength' => 7,
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/min-length', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'minLength' => 12,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/uppercase', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/lowercase', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/number', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/special-char', 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(12, $response['body']['authPasswordPolicyMinLength']);
|
||||
$this->assertEquals(true, $response['body']['authPasswordPolicyRequireUppercase']);
|
||||
$this->assertEquals(true, $response['body']['authPasswordPolicyRequireLowercase']);
|
||||
$this->assertEquals(true, $response['body']['authPasswordPolicyRequireNumber']);
|
||||
$this->assertEquals(true, $response['body']['authPasswordPolicyRequireSpecialChar']);
|
||||
|
||||
$weakPassword = 'password123!';
|
||||
$validPassword = 'Password123!';
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
|
||||
$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() . 'weak-account@localhost.test',
|
||||
'password' => $weakPassword,
|
||||
'name' => 'Weak Account',
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $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() . 'weak-user@localhost.test',
|
||||
'password' => $weakPassword,
|
||||
'name' => 'Weak User',
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$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' => $email,
|
||||
'password' => $validPassword,
|
||||
'name' => 'Password Policy User',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
$userId = $response['body']['$id'];
|
||||
|
||||
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
], [
|
||||
'email' => $email,
|
||||
'password' => $validPassword,
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $session['headers']['status-code']);
|
||||
$session = $session['cookies']['a_session_' . $id];
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
'cookie' => 'a_session_' . $id . '=' . $session,
|
||||
]), [
|
||||
'oldPassword' => $validPassword,
|
||||
'password' => $weakPassword,
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/recovery', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
]), [
|
||||
'email' => $email,
|
||||
'url' => 'http://localhost/recovery',
|
||||
]);
|
||||
|
||||
$this->assertEquals(201, $response['headers']['status-code']);
|
||||
|
||||
$lastEmail = $this->getLastEmailByAddress($email, function ($email) {
|
||||
$this->assertStringContainsString('Password Reset', $email['subject']);
|
||||
});
|
||||
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PUT, '/account/recovery', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $id,
|
||||
]), [
|
||||
'userId' => $userId,
|
||||
'secret' => $tokens['secret'],
|
||||
'password' => $weakPassword,
|
||||
]);
|
||||
|
||||
$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' => $weakPassword,
|
||||
]);
|
||||
|
||||
$this->assertEquals(400, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/min-length', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'minLength' => 8,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/uppercase', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/lowercase', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/number', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'enabled' => false,
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $response['headers']['status-code']);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-policy/special-char', 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(8, $response['body']['authPasswordPolicyMinLength']);
|
||||
$this->assertEquals(false, $response['body']['authPasswordPolicyRequireUppercase']);
|
||||
$this->assertEquals(false, $response['body']['authPasswordPolicyRequireLowercase']);
|
||||
$this->assertEquals(false, $response['body']['authPasswordPolicyRequireNumber']);
|
||||
$this->assertEquals(false, $response['body']['authPasswordPolicyRequireSpecialChar']);
|
||||
}
|
||||
|
||||
#[Group('smtpAndTemplates')]
|
||||
#[Group('projectsCRUD')]
|
||||
public function testUpdateMockNumbers(): void
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Auth\Validator;
|
||||
|
||||
use Appwrite\Auth\Validator\PasswordPolicy;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PasswordPolicyTest extends TestCase
|
||||
{
|
||||
public function testDefaultPolicy(): void
|
||||
{
|
||||
$validator = new PasswordPolicy();
|
||||
|
||||
$this->assertFalse($validator->isValid('1234567'));
|
||||
$this->assertTrue($validator->isValid('password'));
|
||||
}
|
||||
|
||||
public function testConfiguredPolicy(): void
|
||||
{
|
||||
$validator = new PasswordPolicy([
|
||||
'minLength' => 12,
|
||||
'requireUppercase' => true,
|
||||
'requireLowercase' => true,
|
||||
'requireNumber' => true,
|
||||
'requireSpecialChar' => true,
|
||||
]);
|
||||
|
||||
$this->assertFalse($validator->isValid('Password1!'));
|
||||
$this->assertFalse($validator->isValid('password123!'));
|
||||
$this->assertFalse($validator->isValid('PASSWORD123!'));
|
||||
$this->assertFalse($validator->isValid('PasswordOnly!'));
|
||||
$this->assertFalse($validator->isValid('Password1234'));
|
||||
$this->assertTrue($validator->isValid('Password123!'));
|
||||
}
|
||||
|
||||
public function testAllowEmpty(): void
|
||||
{
|
||||
$validator = new PasswordPolicy([
|
||||
'minLength' => 12,
|
||||
'requireUppercase' => true,
|
||||
'requireLowercase' => true,
|
||||
'requireNumber' => true,
|
||||
'requireSpecialChar' => true,
|
||||
], true);
|
||||
|
||||
$this->assertTrue($validator->isValid(''));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user