Compare commits

...
2 changed files with 81 additions and 65 deletions
+80 -64
View File
@@ -160,8 +160,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
->setAttribute('current', true)
->setAttribute('countryName', $countryName)
->setAttribute('expire', $expire)
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $sessionSecret) : '')
;
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $sessionSecret) : '');
$response->dynamic($session, Response::MODEL_SESSION);
};
@@ -228,7 +227,8 @@ App::post('/v1/account')
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
/** Return a generic bad request to prevent exposing existing accounts */
throw new Exception(Exception::GENERAL_BAD_REQUEST);
}
if ($project->getAttribute('auths', [])['personalDataCheck'] ?? false) {
@@ -405,7 +405,8 @@ App::get('/v1/account/sessions')
$sessions = $user->getAttribute('sessions', []);
$current = Auth::sessionVerify($sessions, Auth::$secret);
foreach ($sessions as $key => $session) {/** @var Document $session */
foreach ($sessions as $key => $session) {
/** @var Document $session */
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session->setAttribute('countryName', $countryName);
@@ -447,7 +448,8 @@ App::delete('/v1/account/sessions')
$protocol = $request->getProtocol();
$sessions = $user->getAttribute('sessions', []);
foreach ($sessions as $session) {/** @var Document $session */
foreach ($sessions as $session) {
/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
if (!Config::getParam('domainVerification')) {
@@ -515,15 +517,15 @@ App::get('/v1/account/sessions/:sessionId')
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
: $sessionId;
foreach ($sessions as $session) {/** @var Document $session */
foreach ($sessions as $session) {
/** @var Document $session */
if ($sessionId === $session->getId()) {
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session
->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret)))
->setAttribute('countryName', $countryName)
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $session->getAttribute('secret', '') : '')
;
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $session->getAttribute('secret', '') : '');
return $response->dynamic($session, Response::MODEL_SESSION);
}
@@ -682,8 +684,7 @@ App::patch('/v1/account/sessions/:sessionId')
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION))
;
->setPayload($response->output($session, Response::MODEL_SESSION));
return $response->dynamic($session, Response::MODEL_SESSION);
});
@@ -786,8 +787,7 @@ App::post('/v1/account/sessions/email')
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
;
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
}
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
@@ -795,21 +795,18 @@ App::post('/v1/account/sessions/email')
$response
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
->setStatusCode(Response::STATUS_CODE_CREATED);
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session
->setAttribute('current', true)
->setAttribute('countryName', $countryName)
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $secret) : '')
;
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $secret) : '');
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
->setParam('sessionId', $session->getId());
$response->dynamic($session, Response::MODEL_SESSION);
});
@@ -916,7 +913,7 @@ App::post('/v1/account/sessions/anonymous')
Authorization::setRole(Role::user($user->getId())->toString());
$session = $dbForProject->createDocument('sessions', $session-> setAttribute('$permissions', [
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
@@ -926,8 +923,7 @@ App::post('/v1/account/sessions/anonymous')
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
;
->setParam('sessionId', $session->getId());
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
@@ -938,16 +934,14 @@ App::post('/v1/account/sessions/anonymous')
$response
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
->setStatusCode(Response::STATUS_CODE_CREATED);
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
$session
->setAttribute('current', true)
->setAttribute('countryName', $countryName)
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $secret) : '')
;
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $secret) : '');
$response->dynamic($session, Response::MODEL_SESSION);
});
@@ -1338,7 +1332,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
/** Return a generic bad request to prevent exposing existing accounts */
throw new Exception(Exception::GENERAL_BAD_REQUEST);
}
try {
@@ -1409,7 +1404,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
Query::notEqual('userInternalId', $user->getInternalId()),
]);
if (!empty($identitiesWithMatchingEmail)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
/** Return a generic bad request to prevent exposing existing accounts */
throw new Exception(Exception::GENERAL_BAD_REQUEST);
}
$dbForProject->createDocument('identities', new Document([
@@ -1482,8 +1478,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$queueForEvents
->setEvent('users.[userId].tokens.[tokenId].create')
->setParam('userId', $user->getId())
->setParam('tokenId', $token->getId())
;
->setParam('tokenId', $token->getId());
$query['secret'] = $secret;
$query['userId'] = $user->getId();
@@ -1526,8 +1521,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION))
;
->setPayload($response->output($session, Response::MODEL_SESSION));
// TODO: Remove this deprecated workaround - support only token
if ($state['success']['path'] == $oauthDefaultSuccess) {
@@ -1566,8 +1560,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
->redirect($state['success'])
;
->redirect($state['success']);
});
App::get('/v1/account/tokens/oauth2/:provider')
@@ -1943,7 +1936,8 @@ App::post('/v1/account/tokens/email')
Query::equal('providerEmail', [$email]),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
/** Return a generic bad request to prevent exposing existing accounts */
throw new Exception(Exception::GENERAL_BAD_REQUEST);
}
$userId = $userId === 'unique()' ? ID::unique() : $userId;
@@ -2356,15 +2350,16 @@ App::post('/v1/account/jwt')
->label('abuse-key', 'url:{url},userId:{userId}')
->inject('response')
->inject('user')
->inject('dbForProject')
->action(function (Response $response, Document $user, Database $dbForProject) {
->action(function (Response $response, Document $user) {
$sessions = $user->getAttribute('sessions', []);
$current = new Document();
foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
foreach ($sessions as $session) {
/** @var Document $session */
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) {
// If current session delete the cookies too
$current = $session;
}
}
@@ -2373,15 +2368,12 @@ App::post('/v1/account/jwt')
throw new Exception(Exception::USER_SESSION_NOT_FOUND);
}
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
// Instantiate with key, algo, maxAge and leeway.
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10);
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic(new Document(['jwt' => $jwt->encode([
// 'uid' => 1,
// 'aud' => 'http://site.com',
// 'scopes' => ['user'],
// 'iss' => 'http://api.mysite.com',
'userId' => $user->getId(),
'sessionId' => $current->getId(),
])]), Response::MODEL_JWT);
@@ -2403,7 +2395,6 @@ App::get('/v1/account/prefs')
->inject('response')
->inject('user')
->action(function (Response $response, Document $user) {
$prefs = $user->getAttribute('prefs', []);
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
@@ -2509,7 +2500,7 @@ App::patch('/v1/account/name')
App::patch('/v1/account/password')
->desc('Update password')
->groups(['api', 'account'])
->groups(['api', 'account', 'credentials'])
->label('event', 'users.[userId].update.password')
->label('scope', 'account')
->label('audits.event', 'user.update')
@@ -2624,7 +2615,8 @@ App::patch('/v1/account/email')
Query::notEqual('userInternalId', $user->getInternalId()),
]);
if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
throw new Exception(Exception::GENERAL_BAD_REQUEST);
/** Return a generic bad request to prevent exposing existing accounts */
}
$user
@@ -2660,7 +2652,8 @@ App::patch('/v1/account/email')
}
$dbForProject->purgeCachedDocument('users', $user->getId());
} catch (Duplicate) {
throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */
throw new Exception(Exception::GENERAL_BAD_REQUEST);
/** Return a generic bad request to prevent exposing existing accounts */
}
$queueForEvents->setParam('userId', $user->getId());
@@ -2819,8 +2812,7 @@ App::patch('/v1/account/status')
$protocol = $request->getProtocol();
$response
->addCookie(Auth::$cookieName . '_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
;
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'));
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
@@ -3063,12 +3055,12 @@ App::put('/v1/account/recovery')
$hooks->trigger('passwordValidator', [$dbForProject, $project, $password, &$user, true]);
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('emailVerification', true));
->setAttribute('password', $newPassword)
->setAttribute('passwordHistory', $history)
->setAttribute('passwordUpdate', DateTime::now())
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('emailVerification', true));
$user->setAttributes($profile->getArrayCopy());
@@ -3083,8 +3075,7 @@ App::put('/v1/account/recovery')
$queueForEvents
->setParam('userId', $profile->getId())
->setParam('tokenId', $recoveryDocument->getId())
;
->setParam('tokenId', $recoveryDocument->getId());
$response->dynamic($recoveryDocument, Response::MODEL_TOKEN);
});
@@ -3477,8 +3468,7 @@ App::put('/v1/account/verification/phone')
$queueForEvents
->setParam('userId', $user->getId())
->setParam('tokenId', $verificationDocument->getId())
;
->setParam('tokenId', $verificationDocument->getId());
$response->dynamic($verificationDocument, Response::MODEL_TOKEN);
});
@@ -4147,8 +4137,8 @@ App::put('/v1/account/mfa/challenge')
$dbForProject->updateDocument('sessions', $session->getId(), $session);
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId());
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId());
$response->dynamic($session, Response::MODEL_SESSION);
});
@@ -4203,7 +4193,7 @@ App::post('/v1/account/targets/push')
],
'providerId' => !empty($providerId) ? $providerId : null,
'providerInternalId' => !empty($providerId) ? $provider->getInternalId() : null,
'providerType' => MESSAGE_TYPE_PUSH,
'providerType' => MESSAGE_TYPE_PUSH,
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'sessionId' => $session->getId(),
@@ -4354,8 +4344,8 @@ App::get('/v1/account/identities')
$queries[] = Query::equal('userInternalId', [$user->getInternalId()]);
/**
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
* Get cursor document if there was a cursor query, we use array_filter and reset for reference $cursor to $queries
*/
$cursor = \array_filter($queries, function ($query) {
return \in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
});
@@ -4418,3 +4408,29 @@ App::delete('/v1/account/identities/:identityId')
return $response->noContent();
});
App::shutdown()
->groups(['credentials'])
->desc('Delete existing sessions on credential changes')
->inject('dbForProject')
->inject('queueForDeletes')
->inject('user')
->action(function (Database $dbForProject, Delete $queueForDeletes, Document $user) {
foreach ($user->getAttribute('sessions', []) as $session) {
$session = $dbForProject->getDocument('sessions', $session);
if ($session->isEmpty()) {
continue;
}
$queueForDeletes
->setType(DELETE_TYPE_SESSION_TARGETS)
->setDocument($session)
->trigger();
$dbForProject->deleteDocument(
'sessions',
$session->getId()
);
}
});
+1 -1
View File
@@ -1097,7 +1097,7 @@ App::patch('/v1/users/:userId/name')
App::patch('/v1/users/:userId/password')
->desc('Update password')
->groups(['api', 'users'])
->groups(['api', 'users', 'credentials'])
->label('event', 'users.[userId].update.password')
->label('scope', 'users.write')
->label('audits.event', 'user.update')