mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Merge branch '1.8.x' into PLA-3576
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Utopia\Auth\Hashes\Argon2;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
|
||||
@@ -173,7 +173,7 @@ return [
|
||||
'size' => 256,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => (new Argon2())->getName(),
|
||||
'default' => Auth::DEFAULT_ALGO,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
@@ -184,7 +184,7 @@ return [
|
||||
'size' => 65535,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => (new Argon2())->getOptions(),
|
||||
'default' => Auth::DEFAULT_ALGO_OPTIONS,
|
||||
'array' => false,
|
||||
'filters' => ['json'],
|
||||
],
|
||||
@@ -1115,9 +1115,9 @@ return [
|
||||
[
|
||||
'$id' => ID::custom('expire'),
|
||||
'type' => Database::VAR_DATETIME,
|
||||
'format' => '',
|
||||
'size' => 0,
|
||||
'required' => false,
|
||||
'format' => '',
|
||||
'signed' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Initializes console project document.
|
||||
*/
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Network\Platform;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\System\System;
|
||||
@@ -37,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' => TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG, // 1 Year in seconds
|
||||
'sessionAlerts' => System::getEnv('_APP_CONSOLE_SESSION_ALERTS', 'disabled') === 'enabled',
|
||||
'invalidateSessions' => true
|
||||
],
|
||||
|
||||
@@ -79,7 +79,7 @@ return [
|
||||
[
|
||||
'key' => 'apple',
|
||||
'name' => 'Apple',
|
||||
'version' => '13.2.1',
|
||||
'version' => '13.2.2',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-apple',
|
||||
'package' => 'https://github.com/appwrite/sdk-for-apple',
|
||||
'enabled' => true,
|
||||
@@ -418,7 +418,7 @@ return [
|
||||
[
|
||||
'key' => 'swift',
|
||||
'name' => 'Swift',
|
||||
'version' => '13.2.1',
|
||||
'version' => '13.2.2',
|
||||
'url' => 'https://github.com/appwrite/sdk-for-swift',
|
||||
'package' => 'https://github.com/appwrite/sdk-for-swift',
|
||||
'enabled' => true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Appwrite\Auth\Auth;
|
||||
|
||||
$member = [
|
||||
'global',
|
||||
@@ -91,7 +92,7 @@ $admins = [
|
||||
];
|
||||
|
||||
return [
|
||||
USER_ROLE_GUESTS => [
|
||||
Auth::USER_ROLE_GUESTS => [
|
||||
'label' => 'Guests',
|
||||
'scopes' => [
|
||||
'global',
|
||||
@@ -111,23 +112,23 @@ return [
|
||||
'execution.write',
|
||||
],
|
||||
],
|
||||
USER_ROLE_USERS => [
|
||||
Auth::USER_ROLE_USERS => [
|
||||
'label' => 'Users',
|
||||
'scopes' => \array_merge($member),
|
||||
],
|
||||
USER_ROLE_ADMIN => [
|
||||
Auth::USER_ROLE_ADMIN => [
|
||||
'label' => 'Admin',
|
||||
'scopes' => \array_merge($admins),
|
||||
],
|
||||
USER_ROLE_DEVELOPER => [
|
||||
Auth::USER_ROLE_DEVELOPER => [
|
||||
'label' => 'Developer',
|
||||
'scopes' => \array_merge($admins),
|
||||
],
|
||||
USER_ROLE_OWNER => [
|
||||
Auth::USER_ROLE_OWNER => [
|
||||
'label' => 'Owner',
|
||||
'scopes' => \array_merge($member, $admins),
|
||||
],
|
||||
USER_ROLE_APPS => [
|
||||
Auth::USER_ROLE_APPS => [
|
||||
'label' => 'Applications',
|
||||
'scopes' => ['global', 'health.read', 'graphql'],
|
||||
],
|
||||
|
||||
+174
-267
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\Validator\MockNumber;
|
||||
use Appwrite\Event\Delete;
|
||||
use Appwrite\Event\Mail;
|
||||
@@ -117,7 +118,7 @@ App::post('/v1/projects')
|
||||
'maxSessions' => APP_LIMIT_USER_SESSIONS_DEFAULT,
|
||||
'passwordHistory' => 0,
|
||||
'passwordDictionary' => false,
|
||||
'duration' => TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'duration' => Auth::TOKEN_EXPIRATION_LOGIN_LONG,
|
||||
'personalDataCheck' => false,
|
||||
'mockNumbers' => [],
|
||||
'sessionAlerts' => false,
|
||||
|
||||
@@ -28,9 +28,6 @@ use MaxMind\Db\Reader;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\App;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
@@ -474,9 +471,10 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
|
||||
->param('roles', [], function (Document $project) {
|
||||
if ($project->getId() === 'console') {
|
||||
;
|
||||
$roles = array_keys(Config::getParam('roles', []));
|
||||
$roles = array_filter($roles, function ($role) {
|
||||
return !in_array($role, [USER_ROLE_APPS, USER_ROLE_GUESTS, USER_ROLE_USERS]);
|
||||
array_filter($roles, function ($role) {
|
||||
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
|
||||
});
|
||||
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
}
|
||||
@@ -495,9 +493,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
->inject('timelimit')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('plan')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Password $proofForPassword, Token $proofForToken) {
|
||||
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan) {
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
@@ -570,7 +566,6 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
|
||||
try {
|
||||
$userId = ID::unique();
|
||||
$hash = $proofForPassword->hash($proofForPassword->generate());
|
||||
$invitee = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
|
||||
'$id' => $userId,
|
||||
'$permissions' => [
|
||||
@@ -584,9 +579,9 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
'emailVerification' => false,
|
||||
'status' => true,
|
||||
// TODO: Set password empty?
|
||||
'password' => $hash,
|
||||
'hash' => $proofForPassword->getHash()->getName(),
|
||||
'hashOptions' => $proofForPassword->getHash()->getOptions(),
|
||||
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
|
||||
'hash' => Auth::DEFAULT_ALGO,
|
||||
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
|
||||
/**
|
||||
* Set the password update time to 0 for users created using
|
||||
* team invite and OAuth to allow password updates without an
|
||||
@@ -618,7 +613,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
Query::equal('teamInternalId', [$team->getSequence()]),
|
||||
]);
|
||||
|
||||
$secret = $proofForToken->generate();
|
||||
$secret = Auth::tokenGenerator();
|
||||
if ($membership->isEmpty()) {
|
||||
$membershipId = ID::unique();
|
||||
$membership = new Document([
|
||||
@@ -638,7 +633,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
'invited' => DateTime::now(),
|
||||
'joined' => ($isPrivilegedUser || $isAppUser) ? DateTime::now() : null,
|
||||
'confirm' => ($isPrivilegedUser || $isAppUser),
|
||||
'secret' => $proofForToken->hash($secret),
|
||||
'secret' => Auth::hash($secret),
|
||||
'search' => implode(' ', [$membershipId, $invitee->getId()])
|
||||
]);
|
||||
|
||||
@@ -651,7 +646,7 @@ App::post('/v1/teams/:teamId/memberships')
|
||||
}
|
||||
|
||||
} elseif ($membership->getAttribute('confirm') === false) {
|
||||
$membership->setAttribute('secret', $proofForToken->hash($secret));
|
||||
$membership->setAttribute('secret', Auth::hash($secret));
|
||||
$membership->setAttribute('invited', DateTime::now());
|
||||
|
||||
if ($isPrivilegedUser || $isAppUser) {
|
||||
@@ -1075,7 +1070,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId')
|
||||
if ($project->getId() === 'console') {
|
||||
$roles = array_keys(Config::getParam('roles', []));
|
||||
array_filter($roles, function ($role) {
|
||||
return !in_array($role, [USER_ROLE_APPS, USER_ROLE_GUESTS, USER_ROLE_USERS]);
|
||||
return !in_array($role, [Auth::USER_ROLE_APPS, Auth::USER_ROLE_GUESTS, Auth::USER_ROLE_USERS]);
|
||||
});
|
||||
return new ArrayList(new WhiteList($roles), APP_LIMIT_ARRAY_PARAMS_SIZE);
|
||||
}
|
||||
@@ -1190,9 +1185,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
|
||||
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents) {
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
$membership = $dbForProject->getDocument('memberships', $membershipId);
|
||||
@@ -1211,7 +1204,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
throw new Exception(Exception::TEAM_MEMBERSHIP_MISMATCH);
|
||||
}
|
||||
|
||||
if (!$proofForToken->verify($secret, $membership->getAttribute('secret'))) {
|
||||
if (Auth::hash($secret) !== $membership->getAttribute('secret')) {
|
||||
throw new Exception(Exception::TEAM_INVALID_SECRET);
|
||||
}
|
||||
|
||||
@@ -1245,9 +1238,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
|
||||
$secret = $proofForToken->generate();
|
||||
$secret = Auth::tokenGenerator();
|
||||
$session = new Document(array_merge([
|
||||
'$id' => ID::unique(),
|
||||
'$permissions' => [
|
||||
@@ -1257,9 +1250,9 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
],
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'provider' => SESSION_PROVIDER_EMAIL,
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => $user->getAttribute('email'),
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'factors' => ['email'],
|
||||
@@ -1271,19 +1264,14 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
|
||||
Authorization::setRole(Role::user($userId)->toString());
|
||||
|
||||
$encoded = $store
|
||||
->setProperty('id', $user->getId())
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
if (!Config::getParam('domainVerification')) {
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([$store->getKey() => $encoded]));
|
||||
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
|
||||
}
|
||||
|
||||
$response
|
||||
->addCookie(
|
||||
name: $store->getKey() . '_legacy',
|
||||
value: $encoded,
|
||||
name: Auth::$cookieName . '_legacy',
|
||||
value: Auth::encodeSession($user->getId(), $secret),
|
||||
expire: (new \DateTime($expire))->getTimestamp(),
|
||||
path: '/',
|
||||
domain: Config::getParam('cookieDomain'),
|
||||
@@ -1291,8 +1279,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
httponly: true
|
||||
)
|
||||
->addCookie(
|
||||
name: $store->getKey(),
|
||||
value: $encoded,
|
||||
name: Auth::$cookieName,
|
||||
value: Auth::encodeSession($user->getId(), $secret),
|
||||
expire: (new \DateTime($expire))->getTimestamp(),
|
||||
path: '/',
|
||||
domain: Config::getParam('cookieDomain'),
|
||||
|
||||
+43
-101
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Auth\MFA\Type;
|
||||
use Appwrite\Auth\MFA\Type\TOTP;
|
||||
use Appwrite\Auth\Validator\Password;
|
||||
@@ -31,18 +32,6 @@ use Appwrite\Utopia\Response;
|
||||
use MaxMind\Db\Reader;
|
||||
use Utopia\App;
|
||||
use Utopia\Audit\Audit;
|
||||
use Utopia\Auth\Hash;
|
||||
use Utopia\Auth\Hashes\Argon2;
|
||||
use Utopia\Auth\Hashes\Bcrypt;
|
||||
use Utopia\Auth\Hashes\MD5;
|
||||
use Utopia\Auth\Hashes\PHPass;
|
||||
use Utopia\Auth\Hashes\Plaintext;
|
||||
use Utopia\Auth\Hashes\Scrypt;
|
||||
use Utopia\Auth\Hashes\ScryptModified;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
use Utopia\Auth\Proofs\Password as ProofsPassword;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\DateTime;
|
||||
@@ -71,9 +60,10 @@ use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
||||
/** TODO: Remove function when we move to using utopia/platform */
|
||||
function createUser(Hash $hash, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
|
||||
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Document $project, Database $dbForProject, Hooks $hooks): Document
|
||||
{
|
||||
$plaintextPassword = $password;
|
||||
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
|
||||
$passwordHistory = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
|
||||
if (!empty($email)) {
|
||||
@@ -107,18 +97,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
|
||||
}
|
||||
}
|
||||
|
||||
$hashedPassword = null;
|
||||
|
||||
if (!empty($password)) {
|
||||
if ($hash instanceof Plaintext) { // Password was never hashed, hash it with the default hash
|
||||
$defaultHash = new ProofsPassword();
|
||||
$hashedPassword = $defaultHash->hash($password);
|
||||
$hash = $defaultHash->getHash();
|
||||
} else {
|
||||
$hashedPassword = $password;
|
||||
}
|
||||
}
|
||||
|
||||
$password = (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null;
|
||||
$user = new Document([
|
||||
'$id' => $userId,
|
||||
'$permissions' => [
|
||||
@@ -132,11 +111,11 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
|
||||
'phoneVerification' => false,
|
||||
'status' => true,
|
||||
'labels' => [],
|
||||
'password' => $hashedPassword,
|
||||
'passwordHistory' => is_null($hashedPassword) || $passwordHistory === 0 ? [] : [$hashedPassword],
|
||||
'passwordUpdate' => (!empty($hashedPassword)) ? DateTime::now() : null,
|
||||
'hash' => $hash->getName(),
|
||||
'hashOptions' => $hash->getOptions(),
|
||||
'password' => $password,
|
||||
'passwordHistory' => is_null($password) || $passwordHistory === 0 ? [] : [$password],
|
||||
'passwordUpdate' => (!empty($password)) ? DateTime::now() : null,
|
||||
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
|
||||
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptionsObject + ['type' => $hash],
|
||||
'registration' => DateTime::now(),
|
||||
'reset' => false,
|
||||
'name' => $name,
|
||||
@@ -147,7 +126,7 @@ function createUser(Hash $hash, string $userId, ?string $email, ?string $passwor
|
||||
'search' => implode(' ', [$userId, $email, $phone, $name]),
|
||||
]);
|
||||
|
||||
if ($hash instanceof Plaintext) {
|
||||
if ($hash === 'plaintext') {
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $plaintextPassword, &$user, true]);
|
||||
}
|
||||
|
||||
@@ -238,9 +217,7 @@ App::post('/v1/users')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$plaintext = new Plaintext();
|
||||
|
||||
$user = createUser($plaintext, $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $project, $dbForProject, $hooks);
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($user, Response::MODEL_USER);
|
||||
@@ -274,10 +251,7 @@ App::post('/v1/users/bcrypt')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$bcrypt = new Bcrypt();
|
||||
$bcrypt->setCost(8); // Default cost
|
||||
|
||||
$user = createUser($bcrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -312,9 +286,7 @@ App::post('/v1/users/md5')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$md5 = new MD5();
|
||||
|
||||
$user = createUser($md5, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -349,13 +321,7 @@ App::post('/v1/users/argon2')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$argon2 = new Argon2();
|
||||
$argon2
|
||||
->setMemoryCost(7168)
|
||||
->setTimeCost(5)
|
||||
->setThreads(1);
|
||||
|
||||
$user = createUser($argon2, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -391,12 +357,13 @@ App::post('/v1/users/sha')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$sha = new Sha();
|
||||
$options = '{}';
|
||||
|
||||
if (!empty($passwordVersion)) {
|
||||
$sha->setVersion($passwordVersion);
|
||||
$options = '{"version":"' . $passwordVersion . '"}';
|
||||
}
|
||||
|
||||
$user = createUser($sha, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -431,9 +398,7 @@ App::post('/v1/users/phpass')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$phpass = new PHPass();
|
||||
|
||||
$user = createUser($phpass, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -473,15 +438,15 @@ App::post('/v1/users/scrypt')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$scrypt = new Scrypt();
|
||||
$scrypt
|
||||
->setSalt($passwordSalt)
|
||||
->setCpuCost($passwordCpu)
|
||||
->setMemoryCost($passwordMemory)
|
||||
->setParallelCost($passwordParallel)
|
||||
->setLength($passwordLength);
|
||||
$options = [
|
||||
'salt' => $passwordSalt,
|
||||
'costCpu' => $passwordCpu,
|
||||
'costMemory' => $passwordMemory,
|
||||
'costParallel' => $passwordParallel,
|
||||
'length' => $passwordLength
|
||||
];
|
||||
|
||||
$user = createUser($scrypt, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -519,13 +484,7 @@ App::post('/v1/users/scrypt-modified')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
$scryptModified = new ScryptModified();
|
||||
$scryptModified
|
||||
->setSalt($passwordSalt)
|
||||
->setSaltSeparator($passwordSaltSeparator)
|
||||
->setSignerKey($passwordSignerKey);
|
||||
|
||||
$user = createUser($scryptModified, $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $project, $dbForProject, $hooks);
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
@@ -1110,6 +1069,7 @@ App::get('/v1/users/identities')
|
||||
} catch (QueryException $e) {
|
||||
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$queries[] = Query::search('search', $search);
|
||||
}
|
||||
@@ -1376,21 +1336,12 @@ App::patch('/v1/users/:userId/password')
|
||||
|
||||
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
|
||||
|
||||
// Create Argon2 hasher with default settings
|
||||
$hasher = new Argon2();
|
||||
$hasher
|
||||
->setMemoryCost(7168)
|
||||
->setTimeCost(5)
|
||||
->setThreads(1);
|
||||
$newPassword = Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS);
|
||||
|
||||
$newPassword = $hasher->hash($password);
|
||||
|
||||
$hash = ProofsPassword::createHash($user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
||||
$historyLimit = $project->getAttribute('auths', [])['passwordHistory'] ?? 0;
|
||||
$history = $user->getAttribute('passwordHistory', []);
|
||||
|
||||
if ($historyLimit > 0) {
|
||||
$validator = new PasswordHistory($history, $hash);
|
||||
$validator = new PasswordHistory($history, $user->getAttribute('hash'), $user->getAttribute('hashOptions'));
|
||||
if (!$validator->isValid($password)) {
|
||||
throw new Exception(Exception::USER_PASSWORD_RECENTLY_USED);
|
||||
}
|
||||
@@ -1403,8 +1354,8 @@ App::patch('/v1/users/:userId/password')
|
||||
->setAttribute('password', $newPassword)
|
||||
->setAttribute('passwordHistory', $history)
|
||||
->setAttribute('passwordUpdate', DateTime::now())
|
||||
->setAttribute('hash', $hasher->getName())
|
||||
->setAttribute('hashOptions', $hasher->getOptions());
|
||||
->setAttribute('hash', Auth::DEFAULT_ALGO)
|
||||
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
|
||||
|
||||
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||
|
||||
@@ -2217,19 +2168,17 @@ App::post('/v1/users/:userId/sessions')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
|
||||
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) {
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$secret = $proofForToken->generate();
|
||||
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
|
||||
$session = new Document(array_merge(
|
||||
@@ -2237,8 +2186,8 @@ App::post('/v1/users/:userId/sessions')
|
||||
'$id' => ID::unique(),
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'provider' => SESSION_PROVIDER_SERVER,
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'provider' => Auth::SESSION_PROVIDER_SERVER,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'factors' => ['server'],
|
||||
'ip' => $request->getIP(),
|
||||
@@ -2262,13 +2211,8 @@ App::post('/v1/users/:userId/sessions')
|
||||
|
||||
$dbForProject->purgeCachedDocument('users', $user->getId());
|
||||
|
||||
$encoded = $store
|
||||
->setProperty('id', $user->getId())
|
||||
->setProperty('secret', $secret)
|
||||
->encode();
|
||||
|
||||
$session
|
||||
->setAttribute('secret', $encoded)
|
||||
->setAttribute('secret', Auth::encodeSession($user->getId(), $secret))
|
||||
->setAttribute('countryName', $countryName);
|
||||
|
||||
$queueForEvents
|
||||
@@ -2303,7 +2247,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', TOKEN_EXPIRATION_GENERIC, new Range(60, TOKEN_EXPIRATION_LOGIN_LONG), 'Token expiration period in seconds. The default expiration is 15 minutes.', 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)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
@@ -2315,17 +2259,15 @@ App::post('/v1/users/:userId/tokens')
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
}
|
||||
|
||||
$proofForToken = new Token($length);
|
||||
$proofForToken->setHash(new Sha());
|
||||
$secret = $proofForToken->generate();
|
||||
$secret = Auth::tokenGenerator($length);
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $expire));
|
||||
|
||||
$token = new Document([
|
||||
'$id' => ID::unique(),
|
||||
'userId' => $user->getId(),
|
||||
'userInternalId' => $user->getSequence(),
|
||||
'type' => TOKEN_TYPE_GENERIC,
|
||||
'secret' => $proofForToken->hash($secret),
|
||||
'type' => Auth::TOKEN_TYPE_GENERIC,
|
||||
'secret' => Auth::hash($secret),
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP()
|
||||
|
||||
@@ -1185,29 +1185,17 @@ App::error()
|
||||
$trace = $error->getTrace();
|
||||
|
||||
if (php_sapi_name() === 'cli') {
|
||||
$logLevel = $code >= 500 || $code == 0 ? 'error' : 'warning';
|
||||
$logPrefix = $code >= 500 || $code == 0 ? '[Error]' : '[Warning]';
|
||||
|
||||
Console::{$logLevel}($logPrefix . ' Timestamp: ' . date('c', time()));
|
||||
Console::error('[Error] Timestamp: ' . date('c', time()));
|
||||
|
||||
if ($route) {
|
||||
Console::{$logLevel}($logPrefix . ' Status Code: ' . $code);
|
||||
Console::{$logLevel}($logPrefix . ' URL: ' . $route->getMethod() . ' ' . $route->getPath());
|
||||
Console::error('[Error] Method: ' . $route->getMethod());
|
||||
Console::error('[Error] URL: ' . $route->getPath());
|
||||
}
|
||||
Console::{$logLevel}($logPrefix . ' Type: ' . get_class($error));
|
||||
Console::{$logLevel}($logPrefix . ' Message: ' . $message);
|
||||
Console::{$logLevel}($logPrefix . ' File: ' . $file);
|
||||
Console::{$logLevel}($logPrefix . ' Line: ' . $line);
|
||||
Console::{$logLevel}($logPrefix . ' Trace:');
|
||||
foreach ($trace as $index => $entry) {
|
||||
$traceFile = $entry['file'] ?? 'unknown';
|
||||
$traceLine = $entry['line'] ?? 0;
|
||||
$traceFunction = $entry['function'] ?? 'unknown';
|
||||
$traceClass = $entry['class'] ?? '';
|
||||
$traceType = $entry['type'] ?? '';
|
||||
Console::{$logLevel}(" #{$index} {$traceFile}({$traceLine}): {$traceClass}{$traceType}{$traceFunction}()");
|
||||
}
|
||||
Console::{$logLevel}('');
|
||||
|
||||
Console::error('[Error] Type: ' . get_class($error));
|
||||
Console::error('[Error] Message: ' . $message);
|
||||
Console::error('[Error] File: ' . $file);
|
||||
Console::error('[Error] Line: ' . $line);
|
||||
}
|
||||
|
||||
switch ($class) {
|
||||
|
||||
@@ -222,94 +222,39 @@ App::init()
|
||||
->action(function (App $utopia, Request $request, Database $dbForPlatform, Database $dbForProject, Audit $queueForAudits, Document $project, Document $user, ?Document $session, array $servers, string $mode, Document $team, ?Key $apiKey) {
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
/**
|
||||
* Handle user authentication and session validation.
|
||||
*
|
||||
* This function follows a series of steps to determine the appropriate user session
|
||||
* based on cookies, headers, and JWT tokens.
|
||||
*
|
||||
* Process:
|
||||
*
|
||||
* Project & Role Validation:
|
||||
* 1. Check if the project is empty. If so, throw an exception.
|
||||
* 2. Get the roles configuration.
|
||||
* 3. Determine the role for the user based on the user document.
|
||||
* 4. Get the scopes for the role.
|
||||
*
|
||||
* API Key Authentication:
|
||||
* 5. If there is an API key:
|
||||
* - Verify no user session exists simultaneously
|
||||
* - Check if key is expired
|
||||
* - Set role and scopes from API key
|
||||
* - Handle special app role case
|
||||
* - For standard keys, update last accessed time
|
||||
*
|
||||
* User Activity:
|
||||
* 6. If the project is not the console and user is not admin:
|
||||
* - Update user's last activity timestamp
|
||||
*
|
||||
* Access Control:
|
||||
* 7. Get the method from the route
|
||||
* 8. Validate namespace permissions
|
||||
* 9. Validate scope permissions
|
||||
* 10. Check if user is blocked
|
||||
*
|
||||
* Security Checks:
|
||||
* 11. Verify password status (check if reset required)
|
||||
* 12. Validate MFA requirements:
|
||||
* - Check if MFA is enabled
|
||||
* - Verify email status
|
||||
* - Verify phone status
|
||||
* - Verify authenticator status
|
||||
* 13. Handle Multi-Factor Authentication:
|
||||
* - Check remaining required factors
|
||||
* - Validate factor completion
|
||||
* - Throw exception if factors incomplete
|
||||
*/
|
||||
|
||||
// Step 1: Check if project is empty
|
||||
if ($project->isEmpty()) {
|
||||
throw new Exception(Exception::PROJECT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Step 2: Get roles configuration
|
||||
$roles = Config::getParam('roles', []);
|
||||
|
||||
// Step 3: Determine role for user
|
||||
// TODO get scopes from the identity instead of the user roles config. The identity will containn the scopes the user authorized for the access token.
|
||||
|
||||
$role = $user->isEmpty()
|
||||
? Role::guests()->toString()
|
||||
: Role::users()->toString();
|
||||
|
||||
// Step 4: Get scopes for the role
|
||||
$scopes = $roles[$role]['scopes'];
|
||||
|
||||
// Step 5: API Key Authentication
|
||||
// API Key authentication
|
||||
if (!empty($apiKey)) {
|
||||
// Verify no user session exists simultaneously
|
||||
if (!$user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_API_KEY_AND_SESSION_SET);
|
||||
}
|
||||
// Check if key is expired
|
||||
if ($apiKey->isExpired()) {
|
||||
throw new Exception(Exception::PROJECT_KEY_EXPIRED);
|
||||
}
|
||||
|
||||
// Set role and scopes from API key
|
||||
$role = $apiKey->getRole();
|
||||
$scopes = $apiKey->getScopes();
|
||||
|
||||
|
||||
// Handle special app role case
|
||||
if ($apiKey->getRole() === USER_ROLE_APPS) {
|
||||
if ($apiKey->getRole() === Auth::USER_ROLE_APPS) {
|
||||
// Disable authorization checks for API keys
|
||||
Authorization::setDefaultStatus(false);
|
||||
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'type' => ACTIVITY_TYPE_APP,
|
||||
'type' => Auth::ACTIVITY_TYPE_APP,
|
||||
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => $apiKey->getName(),
|
||||
@@ -318,7 +263,6 @@ App::init()
|
||||
$queueForAudits->setUser($user);
|
||||
}
|
||||
|
||||
// For standard keys, update last accessed time
|
||||
if ($apiKey->getType() === API_KEY_STANDARD) {
|
||||
$dbKey = $project->find(
|
||||
key: 'secret',
|
||||
@@ -388,7 +332,7 @@ App::init()
|
||||
Authorization::setRole($authRole);
|
||||
}
|
||||
|
||||
// Step 6: Update project and user last activity
|
||||
// Update project last activity
|
||||
if (!$project->isEmpty() && $project->getId() !== 'console') {
|
||||
$accessedAt = $project->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_PROJECT_ACCESS)) > $accessedAt) {
|
||||
@@ -397,6 +341,7 @@ App::init()
|
||||
}
|
||||
}
|
||||
|
||||
// Update user last activity
|
||||
if (!empty($user->getId())) {
|
||||
$accessedAt = $user->getAttribute('accessedAt', 0);
|
||||
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_USER_ACCESS)) > $accessedAt) {
|
||||
@@ -410,7 +355,6 @@ App::init()
|
||||
}
|
||||
}
|
||||
|
||||
// Steps 7-9: Access Control - Method, Namespace and Scope Validation
|
||||
/**
|
||||
* @var ?Method $method
|
||||
*/
|
||||
@@ -434,23 +378,21 @@ App::init()
|
||||
}
|
||||
}
|
||||
|
||||
// Step 9: Validate scope permissions
|
||||
// Do now allow access if scope is not allowed
|
||||
$allowed = (array)$route->getLabel('scope', 'none');
|
||||
if (empty(\array_intersect($allowed, $scopes))) {
|
||||
throw new Exception(Exception::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scopes (' . \json_encode($allowed) . ')');
|
||||
}
|
||||
|
||||
// Step 10: Check if user is blocked
|
||||
// Do not allow access to blocked accounts
|
||||
if (false === $user->getAttribute('status')) { // Account is blocked
|
||||
throw new Exception(Exception::USER_BLOCKED);
|
||||
}
|
||||
|
||||
// Step 11: Verify password status
|
||||
if ($user->getAttribute('reset')) {
|
||||
throw new Exception(Exception::USER_PASSWORD_RESET_REQUIRED);
|
||||
}
|
||||
|
||||
// Step 12: Validate MFA requirements
|
||||
$mfaEnabled = $user->getAttribute('mfa', false);
|
||||
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
|
||||
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
|
||||
@@ -458,7 +400,6 @@ App::init()
|
||||
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
|
||||
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;
|
||||
|
||||
// Step 13: Handle Multi-Factor Authentication
|
||||
if (!in_array('mfa', $route->getGroups())) {
|
||||
if ($session && \count($session->getAttribute('factors', [])) < $minimumFactors) {
|
||||
throw new Exception(Exception::USER_MORE_FACTORS_REQUIRED);
|
||||
@@ -585,7 +526,7 @@ App::init()
|
||||
if (!$user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
|
||||
$queueForAudits->setUser($userClone);
|
||||
}
|
||||
|
||||
@@ -825,7 +766,7 @@ App::shutdown()
|
||||
if (!$user->isEmpty()) {
|
||||
$userClone = clone $user;
|
||||
// $user doesn't support `type` and can cause unintended effects.
|
||||
$userClone->setAttribute('type', ACTIVITY_TYPE_USER);
|
||||
$userClone->setAttribute('type', Auth::ACTIVITY_TYPE_USER);
|
||||
$queueForAudits->setUser($userClone);
|
||||
} elseif ($queueForAudits->getUser() === null || $queueForAudits->getUser()->isEmpty()) {
|
||||
/**
|
||||
@@ -839,7 +780,7 @@ App::shutdown()
|
||||
$user = new Document([
|
||||
'$id' => '',
|
||||
'status' => true,
|
||||
'type' => ACTIVITY_TYPE_GUEST,
|
||||
'type' => Auth::ACTIVITY_TYPE_GUEST,
|
||||
'email' => 'guest.' . $project->getId() . '@service.' . $request->getHostname(),
|
||||
'password' => '',
|
||||
'name' => 'Guest',
|
||||
|
||||
@@ -20,7 +20,7 @@ App::init()
|
||||
$lastUpdate = $session->getAttribute('mfaUpdatedAt');
|
||||
if (!empty($lastUpdate)) {
|
||||
$now = DateTime::now();
|
||||
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
|
||||
$maxAllowedDate = DateTime::addSeconds(new \DateTime($lastUpdate), Auth::MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
|
||||
|
||||
$isSessionFresh = DateTime::formatTz($maxAllowedDate) >= DateTime::formatTz($now);
|
||||
}
|
||||
|
||||
@@ -92,72 +92,6 @@ const APP_VCS_GITHUB_USERNAME = 'Appwrite';
|
||||
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
|
||||
const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled';
|
||||
|
||||
// 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;
|
||||
|
||||
+27
-86
@@ -24,16 +24,9 @@ use Appwrite\GraphQL\Schema;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Executor\Executor;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\App;
|
||||
use Utopia\Auth\Hashes\Argon2;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
use Utopia\Auth\Proofs\Code;
|
||||
use Utopia\Auth\Proofs\Password;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
@@ -233,93 +226,72 @@ App::setResource('platforms', function (Request $request, Document $console, Doc
|
||||
];
|
||||
}, ['request', 'console', 'project', 'dbForPlatform']);
|
||||
|
||||
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) {
|
||||
App::setResource('user', function ($mode, $project, $console, $request, $response, $dbForProject, $dbForPlatform) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Utopia\Database\Document $project */
|
||||
/** @var Utopia\Database\Database $dbForProject */
|
||||
/** @var Utopia\Database\Database $dbForPlatform */
|
||||
/** @var string $mode */
|
||||
/** @var Utopia\Auth\Store $store */
|
||||
|
||||
/**
|
||||
* Handles user authentication and session validation.
|
||||
*
|
||||
* This function follows a series of steps to determine the appropriate user session
|
||||
* based on cookies, headers, and JWT tokens.
|
||||
*
|
||||
* Process:
|
||||
* 1. Checks the cookie based on mode:
|
||||
* - If in admin mode, uses console project id for key.
|
||||
* - Otherwise, sets the key using the project ID
|
||||
* 2. If no cookie is found, attempts to retrieve the fallback header `x-fallback-cookies`.
|
||||
* - If this method is used, returns the header: `X-Debug-Fallback: true`.
|
||||
* 3. Fetches the user document from the appropriate database based on the mode.
|
||||
* 4. If the user document is empty or the session key cannot be verified, sets an empty user document.
|
||||
* 5. Regardless of the results from steps 1-4, attempts to fetch the JWT token.
|
||||
* 6. If the JWT user has a valid session ID, updates the user variable with the user from `projectDB`,
|
||||
* overwriting the previous value.
|
||||
*/
|
||||
|
||||
Authorization::setDefaultStatus(true);
|
||||
|
||||
$store->setKey('a_session_' . $project->getId());
|
||||
Auth::setCookieName('a_session_' . $project->getId());
|
||||
|
||||
if (APP_MODE_ADMIN === $mode) {
|
||||
$store->setKey('a_session_' . $console->getId());
|
||||
Auth::setCookieName('a_session_' . $console->getId());
|
||||
}
|
||||
|
||||
$store->decode(
|
||||
$session = Auth::decodeSession(
|
||||
$request->getCookie(
|
||||
$store->getKey(), // Get sessions
|
||||
$request->getCookie($store->getKey() . '_legacy', '')
|
||||
Auth::$cookieName, // Get sessions
|
||||
$request->getCookie(Auth::$cookieName . '_legacy', '')
|
||||
)
|
||||
);
|
||||
|
||||
// Get session from header for SSR clients
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
if (empty($session['id']) && empty($session['secret'])) {
|
||||
$sessionHeader = $request->getHeader('x-appwrite-session', '');
|
||||
|
||||
if (!empty($sessionHeader)) {
|
||||
$store->decode($sessionHeader);
|
||||
$session = Auth::decodeSession($sessionHeader);
|
||||
}
|
||||
}
|
||||
|
||||
// Get fallback session from old clients (no SameSite support) or clients who block 3rd-party cookies
|
||||
if ($response) { // if in http context - add debug header
|
||||
if ($response) {
|
||||
$response->addHeader('X-Debug-Fallback', 'false');
|
||||
}
|
||||
|
||||
if (empty($store->getProperty('id', '')) && empty($store->getProperty('secret', ''))) {
|
||||
if (empty($session['id']) && empty($session['secret'])) {
|
||||
if ($response) {
|
||||
$response->addHeader('X-Debug-Fallback', 'true');
|
||||
}
|
||||
$fallback = $request->getHeader('x-fallback-cookies', '');
|
||||
$fallback = \json_decode($fallback, true);
|
||||
$store->decode(((is_array($fallback) && isset($fallback[$store->getKey()])) ? $fallback[$store->getKey()] : ''));
|
||||
$session = Auth::decodeSession(((isset($fallback[Auth::$cookieName])) ? $fallback[Auth::$cookieName] : ''));
|
||||
}
|
||||
|
||||
Auth::$unique = $session['id'] ?? '';
|
||||
Auth::$secret = $session['secret'] ?? '';
|
||||
|
||||
$user = new Document([]);
|
||||
|
||||
if (APP_MODE_ADMIN === $mode) {
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
if ($project->isEmpty()) {
|
||||
$user = new Document([]);
|
||||
} else {
|
||||
if (!empty($store->getProperty('id', ''))) {
|
||||
if ($project->getId() === 'console') {
|
||||
$user = $dbForPlatform->getDocument('users', $store->getProperty('id', ''));
|
||||
} else {
|
||||
$user = $dbForProject->getDocument('users', $store->getProperty('id', ''));
|
||||
}
|
||||
if (!empty(Auth::$unique)) {
|
||||
if ($mode === APP_MODE_ADMIN) {
|
||||
$user = $dbForPlatform->getDocument('users', Auth::$unique);
|
||||
} elseif (!$project->isEmpty()) {
|
||||
if ($project->getId() === 'console') {
|
||||
$user = $dbForPlatform->getDocument('users', Auth::$unique);
|
||||
} else {
|
||||
$user = $dbForProject->getDocument('users', Auth::$unique);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$user->isEmpty() // Check a document has been found in the DB
|
||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), $store->getProperty('secret', ''), $proofForToken)
|
||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
|
||||
) { // Validate user has valid login token
|
||||
$user = new Document([]);
|
||||
}
|
||||
@@ -364,7 +336,7 @@ App::setResource('user', function (string $mode, Document $project, Document $co
|
||||
$dbForPlatform->setMetadata('user', $user->getId());
|
||||
|
||||
return $user;
|
||||
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform', 'store', 'proofForToken']);
|
||||
}, ['mode', 'project', 'console', 'request', 'response', 'dbForProject', 'dbForPlatform']);
|
||||
|
||||
App::setResource('project', function ($dbForPlatform, $request, $console) {
|
||||
/** @var Appwrite\Utopia\Request $request */
|
||||
@@ -382,13 +354,13 @@ App::setResource('project', function ($dbForPlatform, $request, $console) {
|
||||
return $project;
|
||||
}, ['dbForPlatform', 'request', 'console']);
|
||||
|
||||
App::setResource('session', function (Document $user, Store $store, Token $proofForToken) {
|
||||
App::setResource('session', function (Document $user) {
|
||||
if ($user->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$sessionId = Auth::sessionVerify($user->getAttribute('sessions'), $store->getProperty('secret', ''), $proofForToken);
|
||||
$sessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret);
|
||||
|
||||
if (!$sessionId) {
|
||||
return;
|
||||
@@ -401,7 +373,7 @@ App::setResource('session', function (Document $user, Store $store, Token $proof
|
||||
}
|
||||
|
||||
return;
|
||||
}, ['user', 'store', 'proofForToken']);
|
||||
}, ['user']);
|
||||
|
||||
App::setResource('console', function () {
|
||||
return new Document(Config::getParam('console'));
|
||||
@@ -975,37 +947,6 @@ App::setResource('apiKey', function (Request $request, Document $project): ?Key
|
||||
return Key::decode($project, $key);
|
||||
}, ['request', 'project']);
|
||||
|
||||
|
||||
App::setResource('store', function (): Store {
|
||||
return new Store();
|
||||
});
|
||||
|
||||
App::setResource('proofForPassword', function (): Password {
|
||||
$hash = new Argon2();
|
||||
$hash
|
||||
->setMemoryCost(7168)
|
||||
->setTimeCost(5)
|
||||
->setThreads(1);
|
||||
|
||||
$password = new Password();
|
||||
$password
|
||||
->setHash($hash);
|
||||
|
||||
return $password;
|
||||
});
|
||||
|
||||
App::setResource('proofForToken', function (): Token {
|
||||
$token = new Token();
|
||||
$token->setHash(new Sha());
|
||||
return $token;
|
||||
});
|
||||
|
||||
App::setResource('proofForCode', function (): Code {
|
||||
$code = new Code();
|
||||
$code->setHash(new Sha());
|
||||
return $code;
|
||||
});
|
||||
|
||||
App::setResource('executor', fn () => new Executor());
|
||||
|
||||
App::setResource('resourceToken', function ($project, $dbForProject, $request) {
|
||||
|
||||
+5
-17
@@ -16,9 +16,6 @@ use Swoole\Timer;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Abuse\Adapters\TimeLimit\Redis as TimeLimitRedis;
|
||||
use Utopia\App;
|
||||
use Utopia\Auth\Hashes\Sha;
|
||||
use Utopia\Auth\Proofs\Token;
|
||||
use Utopia\Auth\Store;
|
||||
use Utopia\Cache\Adapter\Pool as CachePool;
|
||||
use Utopia\Cache\Adapter\Sharding;
|
||||
use Utopia\Cache\Cache;
|
||||
@@ -681,24 +678,15 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Payload is not valid.');
|
||||
}
|
||||
|
||||
$store = new Store();
|
||||
$session = Auth::decodeSession($message['data']['session']);
|
||||
Auth::$unique = $session['id'] ?? '';
|
||||
Auth::$secret = $session['secret'] ?? '';
|
||||
|
||||
$store->decode($message['data']['session']);
|
||||
|
||||
$user = $database->getDocument('users', $store->getProperty('id', ''));
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* Moving forward, we should try to use our dependency injection container
|
||||
* to inject the proof for token.
|
||||
* This way we will have one source of truth for the proof for token.
|
||||
*/
|
||||
$proofForToken = new Token();
|
||||
$proofForToken->setHash(new Sha());
|
||||
$user = $database->getDocument('users', Auth::$unique);
|
||||
|
||||
if (
|
||||
empty($user->getId()) // Check a document has been found in the DB
|
||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), $store->getProperty('secret', ''), $proofForToken) // Validate user has valid login token
|
||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
|
||||
) {
|
||||
// cookie not valid
|
||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
|
||||
|
||||
Reference in New Issue
Block a user