This commit is contained in:
Eldad Fux
2025-03-17 21:44:31 +01:00
parent fdb83c51f7
commit 8aa5714173
25 changed files with 293 additions and 459 deletions
+2 -2
View File
@@ -173,7 +173,7 @@ return [
'size' => 256,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO,
'default' => '',
'array' => false,
'filters' => [],
],
@@ -184,7 +184,7 @@ return [
'size' => 65535,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO_OPTIONS,
'default' => new \stdClass(),
'array' => false,
'filters' => ['json'],
],
+1 -1
View File
@@ -38,7 +38,7 @@ $console = [
'mockNumbers' => [],
'invites' => System::getEnv('_APP_CONSOLE_INVITES', 'enabled') === 'enabled',
'limit' => (System::getEnv('_APP_CONSOLE_WHITELIST_ROOT', 'enabled') === 'enabled') ? 1 : 0, // limit signup to 1 user
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'duration' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled'
],
'authWhitelistEmails' => (!empty(System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', System::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
+6 -6
View File
@@ -84,7 +84,7 @@ $admins = [
];
return [
Auth::USER_ROLE_GUESTS => [
USER_ROLE_GUESTS => [
'label' => 'Guests',
'scopes' => [
'global',
@@ -102,23 +102,23 @@ return [
'execution.write',
],
],
Auth::USER_ROLE_USERS => [
USER_ROLE_USERS => [
'label' => 'Users',
'scopes' => \array_merge($member),
],
Auth::USER_ROLE_ADMIN => [
USER_ROLE_ADMIN => [
'label' => 'Admin',
'scopes' => \array_merge($admins),
],
Auth::USER_ROLE_DEVELOPER => [
USER_ROLE_DEVELOPER => [
'label' => 'Developer',
'scopes' => \array_merge($admins),
],
Auth::USER_ROLE_OWNER => [
USER_ROLE_OWNER => [
'label' => 'Owner',
'scopes' => \array_merge($member, $admins),
],
Auth::USER_ROLE_APPS => [
USER_ROLE_APPS => [
'label' => 'Applications',
'scopes' => ['global', 'health.read', 'graphql'],
],
+93 -78
View File
@@ -176,17 +176,17 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$user->setAttributes($userFromRequest->getArrayCopy());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$sessionSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$sessionSecret = Auth::tokenGenerator(TOKEN_LENGTH_SESSION);
$factor = (match ($verifiedToken->getAttribute('type')) {
Auth::TOKEN_TYPE_MAGIC_URL,
Auth::TOKEN_TYPE_OAUTH2,
Auth::TOKEN_TYPE_EMAIL => Type::EMAIL,
Auth::TOKEN_TYPE_PHONE => Type::PHONE,
Auth::TOKEN_TYPE_GENERIC => 'token',
TOKEN_TYPE_MAGIC_URL,
TOKEN_TYPE_OAUTH2,
TOKEN_TYPE_EMAIL => Type::EMAIL,
TOKEN_TYPE_PHONE => Type::PHONE,
TOKEN_TYPE_GENERIC => 'token',
default => throw new Exception(Exception::USER_INVALID_TOKEN)
});
@@ -221,11 +221,11 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$dbForProject->purgeCachedDocument('users', $user->getId());
// Magic URL + Email OTP
if ($verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_MAGIC_URL || $verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_EMAIL) {
if ($verifiedToken->getAttribute('type') === TOKEN_TYPE_MAGIC_URL || $verifiedToken->getAttribute('type') === TOKEN_TYPE_EMAIL) {
$user->setAttribute('emailVerification', true);
}
if ($verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_PHONE) {
if ($verifiedToken->getAttribute('type') === TOKEN_TYPE_PHONE) {
$user->setAttribute('phoneVerification', true);
}
@@ -236,8 +236,8 @@ $createSession = function (string $userId, string $secret, Request $request, Res
}
$isAllowedTokenType = match ($verifiedToken->getAttribute('type')) {
Auth::TOKEN_TYPE_MAGIC_URL,
Auth::TOKEN_TYPE_EMAIL => false,
TOKEN_TYPE_MAGIC_URL,
TOKEN_TYPE_EMAIL => false,
default => true
};
@@ -383,8 +383,8 @@ App::post('/v1/account')
'password' => $hash,
'passwordHistory' => $passwordHistory > 0 ? [$password] : [],
'passwordUpdate' => DateTime::now(),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'hash' => $proof->getHash()->getName(),
'hashOptions' => $proof->getHash()->getOptions(),
'registration' => DateTime::now(),
'reset' => false,
'name' => $name,
@@ -821,7 +821,7 @@ App::patch('/v1/account/sessions/:sessionId')
}
// Extend session
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$session->setAttribute('expire', DateTime::addSeconds(new \DateTime(), $authDuration));
// Refresh OAuth access token
@@ -893,7 +893,8 @@ App::post('/v1/account/sessions/email')
->inject('queueForMails')
->inject('hooks')
->inject('store')
->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store) {
->inject('proofForPassword')
->action(function (string $email, string $password, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword) {
$email = \strtolower($email);
$protocol = $request->getProtocol();
@@ -901,7 +902,9 @@ App::post('/v1/account/sessions/email')
Query::equal('email', [$email]),
]);
if ($profile->isEmpty() || empty($profile->getAttribute('passwordUpdate')) || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) {
$userProofForPassword = ProofsPassword::createHash($profile->getAttribute('hash'), $profile->getAttribute('hashOptions'));
if ($profile->isEmpty() || empty($profile->getAttribute('passwordUpdate')) || !$userProofForPassword->verify($password, $profile->getAttribute('password'))) {
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
@@ -917,16 +920,16 @@ App::post('/v1/account/sessions/email')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, false]);
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$secret = Auth::tokenGenerator(TOKEN_LENGTH_SESSION);
$session = new Document(array_merge(
[
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'provider' => SESSION_PROVIDER_EMAIL,
'providerUid' => $email,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
@@ -943,11 +946,11 @@ App::post('/v1/account/sessions/email')
Authorization::setRole(Role::user($user->getId())->toString());
// Re-hash if not using recommended algo
if ($user->getAttribute('hash') !== Auth::DEFAULT_ALGO) {
if ($user->getAttribute('hash') !== $proofForPassword->getHash()->getName()) {
$user
->setAttribute('password', (new ProofsPassword())->hash($password))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
->setAttribute('hash', $proofForPassword->getHash()->getName())
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions());
$dbForProject->updateDocument('users', $user->getId(), $user);
}
@@ -1033,7 +1036,8 @@ App::post('/v1/account/sessions/anonymous')
->inject('geodb')
->inject('queueForEvents')
->inject('store')
->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store) {
->inject('proofForPassword')
->action(function (Request $request, Response $response, Locale $locale, Document $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword) {
$protocol = $request->getProtocol();
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
@@ -1065,8 +1069,8 @@ App::post('/v1/account/sessions/anonymous')
'emailVerification' => false,
'status' => true,
'password' => null,
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
'passwordUpdate' => null,
'registration' => DateTime::now(),
'reset' => false,
@@ -1084,17 +1088,17 @@ App::post('/v1/account/sessions/anonymous')
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
// Create session token
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$secret = Auth::tokenGenerator(TOKEN_LENGTH_SESSION);
$session = new Document(array_merge(
[
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_ANONYMOUS,
'provider' => SESSION_PROVIDER_ANONYMOUS,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
@@ -1350,7 +1354,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('geodb')
->inject('queueForEvents')
->inject('store')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store) use ($oauthDefaultSuccess) {
->inject('proofForPassword')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword) use ($oauthDefaultSuccess) {
$protocol = $request->getProtocol();
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => ''];
@@ -1568,8 +1573,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'emailVerification' => true,
'status' => true, // Email should already be authenticated by OAuth2 provider
'password' => null,
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
'passwordUpdate' => null,
'registration' => DateTime::now(),
'reset' => false,
@@ -1668,17 +1673,17 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$state['success'] = URLParser::parse($state['success']);
$query = URLParser::parseQuery($state['success']['query']);
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
// If the `token` param is set, we will return the token in the query string
if ($state['token']) {
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_OAUTH2);
$secret = Auth::tokenGenerator(TOKEN_LENGTH_OAUTH2);
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_OAUTH2,
'type' => TOKEN_TYPE_OAUTH2,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@@ -1707,7 +1712,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
} else {
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$secret = Auth::tokenGenerator(TOKEN_LENGTH_SESSION);
$session = new Document(array_merge([
'$id' => ID::unique(),
@@ -1902,7 +1907,8 @@ App::post('/v1/account/tokens/magic-url')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
->inject('proofForPassword')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -1951,8 +1957,8 @@ App::post('/v1/account/tokens/magic-url')
'emailVerification' => false,
'status' => true,
'password' => null,
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
'passwordUpdate' => null,
'registration' => DateTime::now(),
'reset' => false,
@@ -1970,14 +1976,14 @@ App::post('/v1/account/tokens/magic-url')
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
}
$tokenSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_MAGIC_URL);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM));
$tokenSecret = Auth::tokenGenerator(TOKEN_LENGTH_MAGIC_URL);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CONFIRM));
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_MAGIC_URL,
'type' => TOKEN_TYPE_MAGIC_URL,
'secret' => Auth::hash($tokenSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@@ -2150,7 +2156,8 @@ App::post('/v1/account/tokens/email')
->inject('locale')
->inject('queueForEvents')
->inject('queueForMails')
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails) {
->inject('proofForPassword')
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@@ -2198,8 +2205,8 @@ App::post('/v1/account/tokens/email')
'emailVerification' => false,
'status' => true,
'password' => null,
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'hash' => $proofForPassword->getHash()->getName(),
'hashOptions' => $proofForPassword->getHash()->getOptions(),
'passwordUpdate' => null,
'registration' => DateTime::now(),
'reset' => false,
@@ -2216,13 +2223,13 @@ App::post('/v1/account/tokens/email')
}
$tokenSecret = Auth::codeGenerator(6);
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_OTP));
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_OTP));
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_EMAIL,
'type' => TOKEN_TYPE_EMAIL,
'secret' => Auth::hash($tokenSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@@ -2549,13 +2556,13 @@ App::post('/v1/account/tokens/phone')
}
$secret ??= Auth::codeGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_OTP));
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_OTP));
$token = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_PHONE,
'type' => TOKEN_TYPE_PHONE,
'secret' => Auth::hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@@ -2861,14 +2868,16 @@ App::patch('/v1/account/password')
->inject('dbForProject')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) {
->inject('proofForPassword')
->action(function (string $password, string $oldPassword, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks, ProofsPassword $proofForPassword) {
$userProofForPassword = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'));
// 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
if (!empty($user->getAttribute('passwordUpdate')) && !$userProofForPassword->verify($oldPassword, $user->getAttribute('password'))) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
$newPassword = $proofForPassword->hash($password);
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$history = $user->getAttribute('passwordHistory', []);
if ($historyLimit > 0) {
@@ -2894,8 +2903,8 @@ App::patch('/v1/account/password')
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
->setAttribute('hash', $proofForPassword->getHash()->getName())
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions());
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
@@ -2938,9 +2947,11 @@ App::patch('/v1/account/email')
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user->getAttribute('passwordUpdate');
$userProofForPassword = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'));
if (
!empty($passwordUpdate) &&
!$proofForPassword->verify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))
!$userProofForPassword->verify($password, $user->getAttribute('password'))
) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
@@ -2967,9 +2978,9 @@ App::patch('/v1/account/email')
if (empty($passwordUpdate)) {
$user
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('password', $proofForPassword->hash($password))
->setAttribute('hash', $proofForPassword->getHash()->getName())
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions())
->setAttribute('passwordUpdate', DateTime::now());
}
@@ -3030,13 +3041,16 @@ App::patch('/v1/account/phone')
->inject('queueForEvents')
->inject('project')
->inject('hooks')
->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks) {
->inject('proofForPassword')
->action(function (string $phone, string $password, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Document $project, Hooks $hooks, ProofsPassword $proofForPassword) {
// passwordUpdate will be empty if the user has never set a password
$passwordUpdate = $user->getAttribute('passwordUpdate');
$userProofForPassword = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'));
if (
!empty($passwordUpdate) &&
!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))
!$userProofForPassword->verify($password, $user->getAttribute('password'))
) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
@@ -3060,9 +3074,9 @@ App::patch('/v1/account/phone')
if (empty($passwordUpdate)) {
$user
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('password', $proofForPassword->hash($password))
->setAttribute('hash', $proofForPassword->getHash()->getName())
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions())
->setAttribute('passwordUpdate', DateTime::now());
}
@@ -3233,14 +3247,14 @@ App::post('/v1/account/recovery')
throw new Exception(Exception::USER_BLOCKED);
}
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_RECOVERY);
$expire = DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_RECOVERY);
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_RECOVERY);
$secret = Auth::tokenGenerator(TOKEN_LENGTH_RECOVERY);
$recovery = new Document([
'$id' => ID::unique(),
'userId' => $profile->getId(),
'userInternalId' => $profile->getInternalId(),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'type' => TOKEN_TYPE_RECOVERY,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@@ -3391,7 +3405,8 @@ App::put('/v1/account/recovery')
->inject('project')
->inject('queueForEvents')
->inject('hooks')
->action(function (string $userId, string $secret, string $password, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks) {
->inject('proofForPassword')
->action(function (string $userId, string $secret, string $password, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents, Hooks $hooks, ProofsPassword $proofForPassword) {
$profile = $dbForProject->getDocument('users', $userId);
if ($profile->isEmpty()) {
@@ -3399,7 +3414,7 @@ App::put('/v1/account/recovery')
}
$tokens = $profile->getAttribute('tokens', []);
$verifiedToken = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_RECOVERY, $secret);
$verifiedToken = Auth::tokenVerify($tokens, TOKEN_TYPE_RECOVERY, $secret);
if (!$verifiedToken) {
throw new Exception(Exception::USER_INVALID_TOKEN);
@@ -3407,7 +3422,7 @@ App::put('/v1/account/recovery')
Authorization::setRole(Role::user($profile->getId())->toString());
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
$newPassword = $proofForPassword->hash($password);
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
$history = $profile->getAttribute('passwordHistory', []);
@@ -3427,8 +3442,8 @@ App::put('/v1/account/recovery')
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('hash', $proofForPassword->getHash()->getName())
->setAttribute('hashOptions', $proofForPassword->getHash()->getOptions())
->setAttribute('emailVerification', true));
$user->setAttributes($profile->getArrayCopy());
@@ -3495,14 +3510,14 @@ App::post('/v1/account/verification')
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$verificationSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_VERIFICATION);
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
$verificationSecret = Auth::tokenGenerator(TOKEN_LENGTH_VERIFICATION);
$expire = DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CONFIRM);
$verification = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_VERIFICATION,
'type' => TOKEN_TYPE_VERIFICATION,
'secret' => Auth::hash($verificationSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@@ -3658,7 +3673,7 @@ App::put('/v1/account/verification')
}
$tokens = $profile->getAttribute('tokens', []);
$verifiedToken = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_VERIFICATION, $secret);
$verifiedToken = Auth::tokenVerify($tokens, TOKEN_TYPE_VERIFICATION, $secret);
if (!$verifiedToken) {
throw new Exception(Exception::USER_INVALID_TOKEN);
@@ -3750,13 +3765,13 @@ App::post('/v1/account/verification/phone')
}
$secret ??= Auth::codeGenerator();
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
$expire = DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CONFIRM);
$verification = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_PHONE,
'type' => TOKEN_TYPE_PHONE,
'secret' => Auth::hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
@@ -3880,7 +3895,7 @@ App::put('/v1/account/verification/phone')
throw new Exception(Exception::USER_NOT_FOUND);
}
$verifiedToken = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_PHONE, $secret);
$verifiedToken = Auth::tokenVerify($user->getAttribute('tokens', []), TOKEN_TYPE_PHONE, $secret);
if (!$verifiedToken) {
throw new Exception(Exception::USER_INVALID_TOKEN);
@@ -4354,7 +4369,7 @@ App::post('/v1/account/mfa/challenge')
->inject('plan')
->action(function (string $factor, Response $response, Database $dbForProject, Document $user, Locale $locale, Document $project, Request $request, Event $queueForEvents, Messaging $queueForMessaging, Mail $queueForMails, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) {
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_CONFIRM);
$expire = DateTime::addSeconds(new \DateTime(), TOKEN_EXPIRATION_CONFIRM);
$code = Auth::codeGenerator();
$challenge = new Document([
'userId' => $user->getId(),
+1 -1
View File
@@ -117,7 +117,7 @@ App::post('/v1/projects')
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
'passwordHistory' => 0,
'passwordDictionary' => false,
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
'personalDataCheck' => false,
'mockNumbers' => [],
'sessionAlerts' => false,
+4 -4
View File
@@ -451,7 +451,7 @@ App::post('/v1/teams/:teamId/memberships')
;
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
return !in_array($role, [USER_ROLE_APPS, USER_ROLE_GUESTS, USER_ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
@@ -1038,7 +1038,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
;
$roles = array_keys(Config::getParam('roles', []));
array_filter($roles, function ($role) {
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
return !in_array($role, [USER_ROLE_APPS, USER_ROLE_GUESTS, USER_ROLE_USERS]);
});
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
}
@@ -1184,7 +1184,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
$secret = Auth::tokenGenerator();
$session = new Document(array_merge([
@@ -1196,7 +1196,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
],
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'provider' => SESSION_PROVIDER_EMAIL,
'providerUid' => $user->getAttribute('email'),
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
+5 -5
View File
@@ -2022,11 +2022,11 @@ App::post('/v1/users/:userId/sessions')
throw new Exception(Exception::USER_NOT_FOUND);
}
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$secret = Auth::tokenGenerator(TOKEN_LENGTH_SESSION);
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
@@ -2034,7 +2034,7 @@ App::post('/v1/users/:userId/sessions')
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => Auth::SESSION_PROVIDER_SERVER,
'provider' => SESSION_PROVIDER_SERVER,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'userAgent' => $request->getUserAgent('UNKNOWN'),
'factors' => ['server'],
@@ -2099,7 +2099,7 @@ App::post('/v1/users/:userId/tokens')
))
->param('userId', '', new UID(), 'User ID.')
->param('length', 6, new Range(4, 128), 'Token length in characters. The default length is 6 characters', true)
->param('expire', Auth::TOKEN_EXPIRATION_GENERIC, new Range(60, Auth::TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->param('expire', TOKEN_EXPIRATION_GENERIC, new Range(60, TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', true)
->inject('request')
->inject('response')
->inject('dbForProject')
@@ -2118,7 +2118,7 @@ App::post('/v1/users/:userId/tokens')
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Auth::TOKEN_TYPE_GENERIC,
'type' => TOKEN_TYPE_GENERIC,
'secret' => Auth::hash($secret),
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
+5 -5
View File
@@ -282,11 +282,11 @@ App::init()
Authorization::setDefaultStatus(false);
// Handle special app role case
if ($apiKey->getRole() === Auth::USER_ROLE_APPS) {
if ($apiKey->getRole() === USER_ROLE_APPS) {
$user = new Document([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_APP,
'type' => ACTIVITY_TYPE_APP,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $apiKey->getName(),
@@ -551,7 +551,7 @@ App::init()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
}
@@ -773,7 +773,7 @@ App::shutdown()
if (!$user->isEmpty()) {
$userClone = clone $user;
// $user doesn't support `type` and can cause unintended effects.
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
$queueForAudits->setUser($userClone);
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
/**
@@ -787,7 +787,7 @@ App::shutdown()
$user = new Document([
'$id' => '',
'status' => true,
'type' => Auth::ACTIVITY_TYPE_GUEST,
'type' => ACTIVITY_TYPE_GUEST,
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => 'Guest',
+1 -1
View File
@@ -20,7 +20,7 @@ App::init()
$lastUpdate = $session->getAttribute('mfaUpdatedAt');
if (!empty($lastUpdate)) {
$now = DateTime::now();
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), Auth::MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$isSessionFresh = DateTime::formatTz($maxAllowedDate) >= DateTime::formatTz($now);
}
+66 -1
View File
@@ -72,6 +72,72 @@ const APP_PLATFORM_SERVER = 'server';
const APP_PLATFORM_CLIENT = 'client';
const APP_PLATFORM_CONSOLE = 'console';
// User Roles
const USER_ROLE_ANY = 'any';
const USER_ROLE_GUESTS = 'guests';
const USER_ROLE_USERS = 'users';
const USER_ROLE_ADMIN = 'admin';
const USER_ROLE_DEVELOPER = 'developer';
const USER_ROLE_OWNER = 'owner';
const USER_ROLE_APPS = 'apps';
const USER_ROLE_SYSTEM = 'system';
/**
* Token Expiration times.
*/
const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */
const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
/**
* Token Lengths.
*/
const TOKEN_LENGTH_MAGIC_URL = 64;
const TOKEN_LENGTH_VERIFICATION = 256;
const TOKEN_LENGTH_RECOVERY = 256;
const TOKEN_LENGTH_OAUTH2 = 64;
const TOKEN_LENGTH_SESSION = 256;
/**
* Token Types.
*/
const TOKEN_TYPE_LOGIN = 1; // Deprecated
const TOKEN_TYPE_VERIFICATION = 2;
const TOKEN_TYPE_RECOVERY = 3;
const TOKEN_TYPE_INVITE = 4;
const TOKEN_TYPE_MAGIC_URL = 5;
const TOKEN_TYPE_PHONE = 6;
const TOKEN_TYPE_OAUTH2 = 7;
const TOKEN_TYPE_GENERIC = 8;
const TOKEN_TYPE_EMAIL = 9; // OTP
/**
* Session Providers.
*/
const SESSION_PROVIDER_EMAIL = 'email';
const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
const SESSION_PROVIDER_PHONE = 'phone';
const SESSION_PROVIDER_OAUTH2 = 'oauth2';
const SESSION_PROVIDER_TOKEN = 'token';
const SESSION_PROVIDER_SERVER = 'server';
/**
* Activity associated with user or the app.
*/
const ACTIVITY_TYPE_APP = 'app';
const ACTIVITY_TYPE_USER = 'user';
const ACTIVITY_TYPE_GUEST = 'guest';
/**
* MFA
*/
const MFA_RECENT_DURATION = 1800; // 30 mins
// Database Reconnect
const DATABASE_RECONNECT_SLEEP = 2;
const DATABASE_RECONNECT_MAX_ATTEMPTS = 10;
@@ -244,7 +310,6 @@ const METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS = '{resourceType}.{resourceInternalId
const METRIC_RESOURCE_TYPE_ID_DEPLOYMENTS_STORAGE = '{resourceType}.{resourceInternalId}.deployments.storage';
// Resource types
const RESOURCE_TYPE_PROJECTS = 'projects';
const RESOURCE_TYPE_FUNCTIONS = 'functions';
const RESOURCE_TYPE_SITES = 'sites';
Generated
+17 -17
View File
@@ -3512,12 +3512,12 @@
"source": {
"type": "git",
"url": "https://github.com/utopia-php/auth.git",
"reference": "966fbfefb27be94e3363f07279787d5cf8a66b95"
"reference": "ed49b9e481030ba5e589140b41a9f4be1486310f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/auth/zipball/966fbfefb27be94e3363f07279787d5cf8a66b95",
"reference": "966fbfefb27be94e3363f07279787d5cf8a66b95",
"url": "https://api.github.com/repos/utopia-php/auth/zipball/ed49b9e481030ba5e589140b41a9f4be1486310f",
"reference": "ed49b9e481030ba5e589140b41a9f4be1486310f",
"shasum": ""
},
"require": {
@@ -3559,7 +3559,7 @@
"issues": "https://github.com/utopia-php/auth/issues",
"source": "https://github.com/utopia-php/auth/tree/dev"
},
"time": "2025-03-16T18:32:00+00:00"
"time": "2025-03-17T19:57:57+00:00"
},
{
"name": "utopia-php/cache",
@@ -4860,16 +4860,16 @@
},
{
"name": "utopia-php/telemetry",
"version": "0.1.0",
"version": "0.1.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/telemetry.git",
"reference": "d35f2f0632f4ee0be63fb7ace6a94a6adda71a80"
"reference": "437f0021777f0e575dfb9e8a1a081b3aed75e33f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/telemetry/zipball/d35f2f0632f4ee0be63fb7ace6a94a6adda71a80",
"reference": "d35f2f0632f4ee0be63fb7ace6a94a6adda71a80",
"url": "https://api.github.com/repos/utopia-php/telemetry/zipball/437f0021777f0e575dfb9e8a1a081b3aed75e33f",
"reference": "437f0021777f0e575dfb9e8a1a081b3aed75e33f",
"shasum": ""
},
"require": {
@@ -4890,7 +4890,7 @@
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\": "src/"
"Utopia\\Telemetry\\": "src/Telemetry"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -4904,9 +4904,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/telemetry/issues",
"source": "https://github.com/utopia-php/telemetry/tree/0.1.0"
"source": "https://github.com/utopia-php/telemetry/tree/0.1.1"
},
"time": "2024-11-13T10:29:53+00:00"
"time": "2025-03-17T11:57:52+00:00"
},
{
"name": "utopia-php/vcs",
@@ -5143,16 +5143,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.40.7",
"version": "0.40.9",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "9e89b0bc4d8e6c81817d27096629f34a149fa873"
"reference": "dbb45a5db22cdc3368fe2573c07ba6088f188fa4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9e89b0bc4d8e6c81817d27096629f34a149fa873",
"reference": "9e89b0bc4d8e6c81817d27096629f34a149fa873",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/dbb45a5db22cdc3368fe2573c07ba6088f188fa4",
"reference": "dbb45a5db22cdc3368fe2573c07ba6088f188fa4",
"shasum": ""
},
"require": {
@@ -5188,9 +5188,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.40.7"
"source": "https://github.com/appwrite/sdk-generator/tree/0.40.9"
},
"time": "2025-03-12T08:43:55+00:00"
"time": "2025-03-17T18:39:14+00:00"
},
{
"name": "doctrine/annotations",
+15 -202
View File
@@ -2,13 +2,6 @@
namespace Appwrite\Auth;
use Appwrite\Auth\Hash\Argon2;
use Appwrite\Auth\Hash\Bcrypt;
use Appwrite\Auth\Hash\Md5;
use Appwrite\Auth\Hash\Phpass;
use Appwrite\Auth\Hash\Scrypt;
use Appwrite\Auth\Hash\Scryptmodified;
use Appwrite\Auth\Hash\Sha;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\Role;
@@ -17,87 +10,6 @@ use Utopia\Database\Validator\Roles;
class Auth
{
public const SUPPORTED_ALGOS = [
'argon2',
'bcrypt',
'md5',
'sha',
'phpass',
'scrypt',
'scryptMod',
'plaintext'
];
public const DEFAULT_ALGO = 'argon2';
public const DEFAULT_ALGO_OPTIONS = ['type' => 'argon2', 'memoryCost' => 2048, 'timeCost' => 4, 'threads' => 3];
/**
* User Roles.
*/
public const USER_ROLE_ANY = 'any';
public const USER_ROLE_GUESTS = 'guests';
public const USER_ROLE_USERS = 'users';
public const USER_ROLE_ADMIN = 'admin';
public const USER_ROLE_DEVELOPER = 'developer';
public const USER_ROLE_OWNER = 'owner';
public const USER_ROLE_APPS = 'apps';
public const USER_ROLE_SYSTEM = 'system';
/**
* Activity associated with user or the app.
*/
public const ACTIVITY_TYPE_APP = 'app';
public const ACTIVITY_TYPE_USER = 'user';
public const ACTIVITY_TYPE_GUEST = 'guest';
/**
* Token Types.
*/
public const TOKEN_TYPE_LOGIN = 1; // Deprecated
public const TOKEN_TYPE_VERIFICATION = 2;
public const TOKEN_TYPE_RECOVERY = 3;
public const TOKEN_TYPE_INVITE = 4;
public const TOKEN_TYPE_MAGIC_URL = 5;
public const TOKEN_TYPE_PHONE = 6;
public const TOKEN_TYPE_OAUTH2 = 7;
public const TOKEN_TYPE_GENERIC = 8;
public const TOKEN_TYPE_EMAIL = 9; // OTP
/**
* Session Providers.
*/
public const SESSION_PROVIDER_EMAIL = 'email';
public const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
public const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
public const SESSION_PROVIDER_PHONE = 'phone';
public const SESSION_PROVIDER_OAUTH2 = 'oauth2';
public const SESSION_PROVIDER_TOKEN = 'token';
public const SESSION_PROVIDER_SERVER = 'server';
/**
* Token Expiration times.
*/
public const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */
public const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
public const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
public const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
/**
* Token Lengths.
*/
public const TOKEN_LENGTH_MAGIC_URL = 64;
public const TOKEN_LENGTH_VERIFICATION = 256;
public const TOKEN_LENGTH_RECOVERY = 256;
public const TOKEN_LENGTH_OAUTH2 = 64;
public const TOKEN_LENGTH_SESSION = 256;
/**
* MFA
*/
public const MFA_RECENT_DURATION = 1800; // 30 mins
/**
* @var string
*/
@@ -109,18 +21,18 @@ class Auth
public static function getSessionProviderByTokenType(int $type): string
{
switch ($type) {
case Auth::TOKEN_TYPE_VERIFICATION:
case Auth::TOKEN_TYPE_RECOVERY:
case Auth::TOKEN_TYPE_INVITE:
return Auth::SESSION_PROVIDER_EMAIL;
case Auth::TOKEN_TYPE_MAGIC_URL:
return Auth::SESSION_PROVIDER_MAGIC_URL;
case Auth::TOKEN_TYPE_PHONE:
return Auth::SESSION_PROVIDER_PHONE;
case Auth::TOKEN_TYPE_OAUTH2:
return Auth::SESSION_PROVIDER_OAUTH2;
case TOKEN_TYPE_VERIFICATION:
case TOKEN_TYPE_RECOVERY:
case TOKEN_TYPE_INVITE:
return SESSION_PROVIDER_EMAIL;
case TOKEN_TYPE_MAGIC_URL:
return SESSION_PROVIDER_MAGIC_URL;
case TOKEN_TYPE_PHONE:
return SESSION_PROVIDER_PHONE;
case TOKEN_TYPE_OAUTH2:
return SESSION_PROVIDER_OAUTH2;
default:
return Auth::SESSION_PROVIDER_TOKEN;
return SESSION_PROVIDER_TOKEN;
}
}
@@ -138,105 +50,6 @@ class Auth
return \hash('sha256', $string);
}
/**
* Password Hash.
*
* One way string hashing for user passwords
*
* @param string $string
* @param string $algo hashing algorithm to use
* @param array $options algo-specific options
*
* @return bool|string|null
*/
public static function passwordHash(string $string, string $algo, array $options = [])
{
// Plain text not supported, just an alias. Switch to recommended algo
if ($algo === 'plaintext') {
$algo = Auth::DEFAULT_ALGO;
$options = Auth::DEFAULT_ALGO_OPTIONS;
}
if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) {
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
switch ($algo) {
case 'argon2':
$hasher = new Argon2($options);
return $hasher->hash($string);
case 'bcrypt':
$hasher = new Bcrypt($options);
return $hasher->hash($string);
case 'md5':
$hasher = new Md5($options);
return $hasher->hash($string);
case 'sha':
$hasher = new Sha($options);
return $hasher->hash($string);
case 'phpass':
$hasher = new Phpass($options);
return $hasher->hash($string);
case 'scrypt':
$hasher = new Scrypt($options);
return $hasher->hash($string);
case 'scryptMod':
$hasher = new Scryptmodified($options);
return $hasher->hash($string);
default:
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
}
/**
* Password verify.
*
* @param string $plain
* @param string $hash
* @param string $algo hashing algorithm used to hash
* @param array $options algo-specific options
*
* @return bool
*/
public static function passwordVerify(string $plain, string $hash, string $algo, array $options = [])
{
// Plain text not supported, just an alias. Switch to recommended algo
if ($algo === 'plaintext') {
$algo = Auth::DEFAULT_ALGO;
$options = Auth::DEFAULT_ALGO_OPTIONS;
}
if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) {
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
switch ($algo) {
case 'argon2':
$hasher = new Argon2($options);
return $hasher->verify($plain, $hash);
case 'bcrypt':
$hasher = new Bcrypt($options);
return $hasher->verify($plain, $hash);
case 'md5':
$hasher = new Md5($options);
return $hasher->verify($plain, $hash);
case 'sha':
$hasher = new Sha($options);
return $hasher->verify($plain, $hash);
case 'phpass':
$hasher = new Phpass($options);
return $hasher->verify($plain, $hash);
case 'scrypt':
$hasher = new Scrypt($options);
return $hasher->verify($plain, $hash);
case 'scryptMod':
$hasher = new Scryptmodified($options);
return $hasher->verify($plain, $hash);
default:
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
}
/**
* Token Generator.
*
@@ -339,9 +152,9 @@ class Auth
public static function isPrivilegedUser(array $roles): bool
{
if (
in_array(self::USER_ROLE_OWNER, $roles) ||
in_array(self::USER_ROLE_DEVELOPER, $roles) ||
in_array(self::USER_ROLE_ADMIN, $roles)
in_array(USER_ROLE_OWNER, $roles) ||
in_array(USER_ROLE_DEVELOPER, $roles) ||
in_array(USER_ROLE_ADMIN, $roles)
) {
return true;
}
@@ -358,7 +171,7 @@ class Auth
*/
public static function isAppUser(array $roles): bool
{
if (in_array(self::USER_ROLE_APPS, $roles)) {
if (in_array(USER_ROLE_APPS, $roles)) {
return true;
}
-62
View File
@@ -1,62 +0,0 @@
<?php
namespace Appwrite\Auth;
abstract class Hash
{
/**
* @var array $options Hashing-algo specific options
*/
protected array $options = [];
/**
* @param array $options Hashing-algo specific options
*/
public function __construct(array $options = [])
{
$this->setOptions($options);
}
/**
* Set hashing algo options
*
* @param array $options Hashing-algo specific options
*/
public function setOptions(array $options): self
{
$this->options = \array_merge([], $this->getDefaultOptions(), $options);
return $this;
}
/**
* Get hashing algo options
*
* @return array $options Hashing-algo specific options
*/
public function getOptions(): array
{
return $this->options;
}
/**
* @param string $password Input password to hash
*
* @return string hash
*/
abstract public function hash(string $password): string;
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
abstract public function verify(string $password, string $hash): bool;
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
abstract public function getDefaultOptions(): array;
}
+4 -4
View File
@@ -104,16 +104,16 @@ class Key
$secret = $key;
}
$role = Auth::USER_ROLE_APPS;
$role = USER_ROLE_APPS;
$roles = Config::getParam('roles', []);
$scopes = $roles[Auth::USER_ROLE_APPS]['scopes'] ?? [];
$scopes = $roles[USER_ROLE_APPS]['scopes'] ?? [];
$expired = false;
$guestKey = new Key(
$project->getId(),
$type,
Auth::USER_ROLE_GUESTS,
$roles[Auth::USER_ROLE_GUESTS]['scopes'] ?? [],
USER_ROLE_GUESTS,
$roles[USER_ROLE_GUESTS]['scopes'] ?? [],
'UNKNOWN'
);
@@ -2,7 +2,7 @@
namespace Appwrite\Auth\Validator;
use Appwrite\Auth\Auth;
use Utopia\Auth\Proofs\Password as ProofsPassword;
/**
* Password.
@@ -45,8 +45,10 @@ class PasswordHistory extends Password
*/
public function isValid($value): bool
{
$proofForPassword = ProofsPassword::createHash($this->algo, $this->algoOptions);
foreach ($this->history as $hash) {
if (!empty($hash) && Auth::passwordVerify($value, $hash, $this->algo, $this->algoOptions)) {
if (!empty($hash) && $proofForPassword->verify($value, $hash)) {
return false;
}
}
+1 -1
View File
@@ -118,7 +118,7 @@ class V16 extends Migration
* Set default authDuration
*/
$document->setAttribute('auths', array_merge($document->getAttribute('auths', []), [
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG
'duration' => TOKEN_EXPIRATION_LOGIN_LONG
]));
/**
+1 -1
View File
@@ -270,7 +270,7 @@ class V17 extends Migration
* Set hashOptions type
*/
$document->setAttribute('hashOptions', array_merge($document->getAttribute('hashOptions', []), [
'type' => $document->getAttribute('hash', Auth::DEFAULT_ALGO)
'type' => $document->getAttribute('hash', 'argon2')
]));
break;
}
+5 -5
View File
@@ -632,15 +632,15 @@ class V20 extends Migration
}
break;
case 'sessions':
$duration = $this->project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $this->project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expire = DateTime::addSeconds(new \DateTime(), $duration);
$document->setAttribute('expire', $expire);
$factors = match ($document->getAttribute('provider')) {
Auth::SESSION_PROVIDER_EMAIL => ['password'],
Auth::SESSION_PROVIDER_PHONE => ['phone'],
Auth::SESSION_PROVIDER_ANONYMOUS => ['anonymous'],
Auth::SESSION_PROVIDER_TOKEN => ['token'],
SESSION_PROVIDER_EMAIL => ['password'],
SESSION_PROVIDER_PHONE => ['phone'],
SESSION_PROVIDER_ANONYMOUS => ['anonymous'],
SESSION_PROVIDER_TOKEN => ['token'],
default => ['email'],
};
+1 -1
View File
@@ -83,7 +83,7 @@ class Audits extends Action
$userName = $user->getAttribute('name', '');
$userEmail = $user->getAttribute('email', '');
$userType = $user->getAttribute('type', Auth::ACTIVITY_TYPE_USER);
$userType = $user->getAttribute('type', ACTIVITY_TYPE_USER);
// Create event data
$eventData = [
+1 -1
View File
@@ -716,7 +716,7 @@ class Deletes extends Action
private function deleteExpiredSessions(Document $project, callable $getProjectDB): void
{
$dbForProject = $getProjectDB($project);
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
$expired = DateTime::addSeconds(new \DateTime(), -1 * $duration);
// Delete Sessions
@@ -105,7 +105,7 @@ class Project extends Model
->addRule('authDuration', [
'type' => self::TYPE_INTEGER,
'description' => 'Session duration in seconds.',
'default' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
'default' => TOKEN_EXPIRATION_LOGIN_LONG,
'example' => 60,
])
->addRule('authLimit', [
@@ -359,7 +359,7 @@ class Project extends Model
$auth = Config::getParam('auth', []);
$document->setAttribute('authLimit', $authValues['limit'] ?? 0);
$document->setAttribute('authDuration', $authValues['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG);
$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('authPasswordDictionary', $authValues['passwordDictionary'] ?? false);
@@ -787,7 +787,7 @@ class ProjectsConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(Auth::TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year
$this->assertEquals(TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year
/**
* Test for SUCCESS
@@ -931,7 +931,7 @@ class ProjectsConsoleClientTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
]);
$this->assertEquals(200, $response['headers']['status-code']);
@@ -944,7 +944,7 @@ class ProjectsConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(Auth::TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year
$this->assertEquals(TOKEN_EXPIRATION_LOGIN_LONG, $response['body']['authDuration']); // 1 Year
return ['projectId' => $projectId];
}
+51 -50
View File
@@ -4,6 +4,7 @@ namespace Tests\Unit\Auth;
use Appwrite\Auth\Auth;
use PHPUnit\Framework\TestCase;
use Utopia\Auth\Proofs\Password;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
@@ -37,7 +38,7 @@ class AuthTest extends TestCase
// Bcrypt - Version Y
$plain = 'secret';
$hash = '$2y$08$PDbMtV18J1KOBI9tIYabBuyUwBrtXPGhLxCy9pWP6xkldVOKLrLKy';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$generatedHash = Password::createHash('bcrypt')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
@@ -45,7 +46,7 @@ class AuthTest extends TestCase
// Bcrypt - Version A
$plain = 'test123';
$hash = '$2a$12$3f2ZaARQ1AmhtQWx2nmQpuXcWfTj1YV2/Hl54e8uKxIzJe3IfwLiu';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$generatedHash = Password::createHash('bcrypt')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
@@ -53,7 +54,7 @@ class AuthTest extends TestCase
// Bcrypt - Cost 5
$plain = 'hello-world';
$hash = '$2a$05$IjrtSz6SN7UJ6Sh3l.b5jODEvEG2LMJTPAHIaLWRvlWx7if3VMkFO';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$generatedHash = Password::createHash('bcrypt')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
@@ -61,7 +62,7 @@ class AuthTest extends TestCase
// Bcrypt - Cost 15
$plain = 'super-secret-password';
$hash = '$2a$15$DS0ZzbsFZYumH/E4Qj5oeOHnBcM3nCCsCA2m4Goigat/0iMVQC4Na';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$generatedHash = Password::createHash('bcrypt')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
@@ -69,7 +70,7 @@ class AuthTest extends TestCase
// MD5 - Short
$plain = 'appwrite';
$hash = '144fa7eaa4904e8ee120651997f70dcc';
$generatedHash = Auth::passwordHash($plain, 'md5');
$generatedHash = Password::createHash('md5')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
@@ -77,7 +78,7 @@ class AuthTest extends TestCase
// MD5 - Long
$plain = 'AppwriteIsAwesomeBackendAsAServiceThatIsAlsoOpenSourced';
$hash = '8410e96cf7ac64e0b84c3f8517a82616';
$generatedHash = Auth::passwordHash($plain, 'md5');
$generatedHash = Password::createHash('md5')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
@@ -85,7 +86,7 @@ class AuthTest extends TestCase
// PHPass
$plain = 'pass123';
$hash = '$P$BVKPmJBZuLch27D4oiMRTEykGLQ9tX0';
$generatedHash = Auth::passwordHash($plain, 'phpass');
$generatedHash = Password::createHash('phpass')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
@@ -93,7 +94,7 @@ class AuthTest extends TestCase
// SHA
$plain = 'developersAreAwesome!';
$hash = '2455118438cb125354b89bb5888346e9bd23355462c40df393fab514bf2220b5a08e4e2d7b85d7327595a450d0ac965cc6661152a46a157c66d681bed20a4735';
$generatedHash = Auth::passwordHash($plain, 'sha');
$generatedHash = Password::createHash('sha')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'sha'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'sha'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'sha'));
@@ -101,7 +102,7 @@ class AuthTest extends TestCase
// Argon2
$plain = 'safe-argon-password';
$hash = '$argon2id$v=19$m=2048,t=3,p=4$MWc5NWRmc2QxZzU2$41mp7rSgBZ49YxLbbxIac7aRaxfp5/e1G45ckwnK0g8';
$generatedHash = Auth::passwordHash($plain, 'argon2');
$generatedHash = Password::createHash('argon2')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'argon2'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'argon2'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'argon2'));
@@ -109,7 +110,7 @@ class AuthTest extends TestCase
// Scrypt
$plain = 'some-scrypt-password';
$hash = 'b448ad7ba88b653b5b56b8053a06806724932d0751988bc9cd0ef7ff059e8ba8a020e1913b7069a650d3f99a1559aba0221f2c277826919513a054e76e339028';
$generatedHash = Auth::passwordHash($plain, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]);
$generatedHash = Password::createHash('scrypt')->setOptions([ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2])->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
@@ -126,7 +127,7 @@ class AuthTest extends TestCase
// Provider #1 (Database)
$plain = 'example-password';
$hash = '$2a$10$3bIGRWUes86CICsuchGLj.e.BqdCdg2/1Ud9LvBhJr0j7Dze8PBdS';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$generatedHash = Password::createHash('bcrypt')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
@@ -134,7 +135,7 @@ class AuthTest extends TestCase
// Provider #2 (Blog)
$plain = 'your-password';
$hash = '$P$BkiNDJTpAWXtpaMhEUhUdrv7M0I1g6.';
$generatedHash = Auth::passwordHash($plain, 'phpass');
$generatedHash = Password::createHash('phpass')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
@@ -147,7 +148,7 @@ class AuthTest extends TestCase
$signerKey = 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==';
$options = [ 'salt' => $salt, 'saltSeparator' => $saltSeparator, 'signerKey' => $signerKey ];
$generatedHash = Auth::passwordHash($plain, 'scryptMod', $options);
$generatedHash = Password::createHash('scryptMod')->hash($plain, $options);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scryptMod', $options));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scryptMod', $options));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scryptMod', $options));
@@ -159,7 +160,7 @@ class AuthTest extends TestCase
// Bcrypt - Cost 5
$plain = 'whatIsMd8?!?';
$generatedHash = Auth::passwordHash($plain, 'md8');
$generatedHash = Password::createHash('md8')->hash($plain);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md8'));
}
@@ -187,14 +188,14 @@ class AuthTest extends TestCase
new Document([
'$id' => ID::custom('token1'),
'secret' => $hash,
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'provider' => SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
]),
new Document([
'$id' => ID::custom('token2'),
'secret' => 'secret2',
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'provider' => SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
]),
@@ -206,14 +207,14 @@ class AuthTest extends TestCase
new Document([ // Correct secret and type time, wrong expire time
'$id' => ID::custom('token1'),
'secret' => $hash,
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'provider' => SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
]),
new Document([
'$id' => ID::custom('token2'),
'secret' => 'secret2',
'provider' => Auth::SESSION_PROVIDER_EMAIL,
'provider' => SESSION_PROVIDER_EMAIL,
'providerUid' => 'test@example.com',
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
]),
@@ -232,13 +233,13 @@ class AuthTest extends TestCase
$tokens1 = [
new Document([
'$id' => ID::custom('token1'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'type' => TOKEN_TYPE_RECOVERY,
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
'secret' => $hash,
]),
new Document([
'$id' => ID::custom('token2'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'type' => TOKEN_TYPE_RECOVERY,
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => 'secret2',
]),
@@ -247,13 +248,13 @@ class AuthTest extends TestCase
$tokens2 = [
new Document([ // Correct secret and type time, wrong expire time
'$id' => ID::custom('token1'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'type' => TOKEN_TYPE_RECOVERY,
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => $hash,
]),
new Document([
'$id' => ID::custom('token2'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'type' => TOKEN_TYPE_RECOVERY,
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => 'secret2',
]),
@@ -262,25 +263,25 @@ class AuthTest extends TestCase
$tokens3 = [ // Correct secret and expire time, wrong type
new Document([
'$id' => ID::custom('token1'),
'type' => Auth::TOKEN_TYPE_INVITE,
'type' => TOKEN_TYPE_INVITE,
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), 60 * 60 * 24)),
'secret' => $hash,
]),
new Document([
'$id' => ID::custom('token2'),
'type' => Auth::TOKEN_TYPE_RECOVERY,
'type' => TOKEN_TYPE_RECOVERY,
'expire' => DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -60 * 60 * 24)),
'secret' => 'secret2',
]),
];
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), $tokens1[0]);
$this->assertEquals(Auth::tokenVerify($tokens1, TOKEN_TYPE_RECOVERY, $secret), $tokens1[0]);
$this->assertEquals(Auth::tokenVerify($tokens1, null, $secret), $tokens1[0]);
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens1, TOKEN_TYPE_RECOVERY, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens2, TOKEN_TYPE_RECOVERY, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens2, TOKEN_TYPE_RECOVERY, 'false-secret'), false);
$this->assertEquals(Auth::tokenVerify($tokens3, TOKEN_TYPE_RECOVERY, $secret), false);
$this->assertEquals(Auth::tokenVerify($tokens3, TOKEN_TYPE_RECOVERY, 'false-secret'), false);
}
public function testIsPrivilegedUser(): void
@@ -288,16 +289,16 @@ class AuthTest extends TestCase
$this->assertEquals(false, Auth::isPrivilegedUser([]));
$this->assertEquals(false, Auth::isPrivilegedUser([Role::guests()->toString()]));
$this->assertEquals(false, Auth::isPrivilegedUser([Role::users()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_ADMIN]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_SYSTEM]));
$this->assertEquals(true, Auth::isPrivilegedUser([USER_ROLE_ADMIN]));
$this->assertEquals(true, Auth::isPrivilegedUser([USER_ROLE_DEVELOPER]));
$this->assertEquals(true, Auth::isPrivilegedUser([USER_ROLE_OWNER]));
$this->assertEquals(false, Auth::isPrivilegedUser([USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isPrivilegedUser([USER_ROLE_SYSTEM]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isPrivilegedUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(false, Auth::isPrivilegedUser([USER_ROLE_APPS, USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isPrivilegedUser([USER_ROLE_APPS, Role::guests()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([USER_ROLE_OWNER, Role::guests()->toString()]));
$this->assertEquals(true, Auth::isPrivilegedUser([USER_ROLE_OWNER, USER_ROLE_ADMIN, USER_ROLE_DEVELOPER]));
}
public function testIsAppUser(): void
@@ -305,16 +306,16 @@ class AuthTest extends TestCase
$this->assertEquals(false, Auth::isAppUser([]));
$this->assertEquals(false, Auth::isAppUser([Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Role::users()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_ADMIN]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER]));
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_SYSTEM]));
$this->assertEquals(false, Auth::isAppUser([USER_ROLE_ADMIN]));
$this->assertEquals(false, Auth::isAppUser([USER_ROLE_DEVELOPER]));
$this->assertEquals(false, Auth::isAppUser([USER_ROLE_OWNER]));
$this->assertEquals(true, Auth::isAppUser([USER_ROLE_APPS]));
$this->assertEquals(false, Auth::isAppUser([USER_ROLE_SYSTEM]));
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Auth::USER_ROLE_APPS]));
$this->assertEquals(true, Auth::isAppUser([Auth::USER_ROLE_APPS, Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([Auth::USER_ROLE_OWNER, Auth::USER_ROLE_ADMIN, Auth::USER_ROLE_DEVELOPER]));
$this->assertEquals(true, Auth::isAppUser([USER_ROLE_APPS, USER_ROLE_APPS]));
$this->assertEquals(true, Auth::isAppUser([USER_ROLE_APPS, Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([USER_ROLE_OWNER, Role::guests()->toString()]));
$this->assertEquals(false, Auth::isAppUser([USER_ROLE_OWNER, USER_ROLE_ADMIN, USER_ROLE_DEVELOPER]));
}
public function testGuestRoles(): void
@@ -394,7 +395,7 @@ class AuthTest extends TestCase
public function testPrivilegedUserRoles(): void
{
Authorization::setRole(Auth::USER_ROLE_OWNER);
Authorization::setRole(USER_ROLE_OWNER);
$user = new Document([
'$id' => ID::custom('123'),
'emailVerification' => true,
@@ -438,7 +439,7 @@ class AuthTest extends TestCase
public function testAppUserRoles(): void
{
Authorization::setRole(Auth::USER_ROLE_APPS);
Authorization::setRole(USER_ROLE_APPS);
$user = new Document([
'$id' => ID::custom('123'),
'memberships' => [
+2 -2
View File
@@ -21,7 +21,7 @@ class KeyTest extends TestCase
'collections.read',
'documents.read',
];
$roleScopes = Config::getParam('roles', [])[Auth::USER_ROLE_APPS]['scopes'];
$roleScopes = Config::getParam('roles', [])[USER_ROLE_APPS]['scopes'];
$key = static::generateKey($projectId, $usage, $scopes);
$project = new Document(['$id' => $projectId,]);
@@ -29,7 +29,7 @@ class KeyTest extends TestCase
$this->assertEquals($projectId, $decoded->getProjectId());
$this->assertEquals(API_KEY_DYNAMIC, $decoded->getType());
$this->assertEquals(Auth::USER_ROLE_APPS, $decoded->getRole());
$this->assertEquals(USER_ROLE_APPS, $decoded->getRole());
$this->assertEquals(\array_merge($scopes, $roleScopes), $decoded->getScopes());
}
@@ -59,7 +59,7 @@ class MessagingChannelsTest extends TestCase
'confirm' => true,
'roles' => [
empty($index % 2)
? Auth::USER_ROLE_ADMIN
? USER_ROLE_ADMIN
: 'member',
]
]
@@ -294,7 +294,7 @@ class MessagingChannelsTest extends TestCase
}
$role = empty($index % 2)
? Auth::USER_ROLE_ADMIN
? USER_ROLE_ADMIN
: 'member';
$permissions = [