From de2a4e754a27ecefedc2a07135f1e783b9b0fb9f Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:56:01 +0000 Subject: [PATCH] feat: alias PUT token methods for backwards compat --- app/controllers/api/account.php | 228 ++++++++++++++++++-------------- 1 file changed, 130 insertions(+), 98 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index c4931ffa47..fb7d5bfb51 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1155,10 +1155,105 @@ App::post('/v1/account/tokens/magic-url') ; }); -App::put('/v1/account/sessions/token') - ->alias('/v1/account/sessions/magic-url') +$createSession = function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) { + $roles = Authorization::getRoles(); + $isPrivilegedUser = Auth::isPrivilegedUser($roles); + $isAppUser = Auth::isAppUser($roles); + + /** @var Utopia\Database\Document $user */ + $userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); + + if ($userFromRequest->isEmpty()) { + throw new Exception(Exception::USER_INVALID_TOKEN); + } + + $verifiedToken = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), null, $secret); + + if (!$verifiedToken) { + throw new Exception(Exception::USER_INVALID_TOKEN); + } + + $user->setAttributes($userFromRequest->getArrayCopy()); + + $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $detector = new Detector($request->getUserAgent('UNKNOWN')); + $record = $geodb->get($request->getIP()); + $sessionSecret = Auth::tokenGenerator(); + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); + + $session = new Document(array_merge( + [ + '$id' => ID::unique(), + 'userId' => $user->getId(), + 'userInternalId' => $user->getInternalId(), + 'provider' => Auth::getSessionProviderByTokenType($verifiedToken->getAttribute('type')), + 'secret' => Auth::hash($sessionSecret), // One way hash encryption to protect DB leak + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', + ], + $detector->getOS(), + $detector->getClient(), + $detector->getDevice() + )); + + Authorization::setRole(Role::user($user->getId())->toString()); + + $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())), + ])); + + $dbForProject->deleteCachedDocument('users', $user->getId()); + Authorization::skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId())); + $dbForProject->deleteCachedDocument('users', $user->getId()); + + if ($verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_MAGIC_URL) { + $user->setAttribute('emailVerification', true); + } + + if ($verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_PHONE) { + $user->setAttribute('phoneVerification', true); + } + + try { + $dbForProject->updateDocument('users', $user->getId(), $user); + } catch (\Throwable $th) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB'); + } + + $queueForEvents + ->setParam('userId', $user->getId()) + ->setParam('sessionId', $session->getId()); + + if (!Config::getParam('domainVerification')) { + $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $sessionSecret)])); + } + + $protocol = $request->getProtocol(); + + $response + ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->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('expire', $expire) + ->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $sessionSecret) : '') + ; + + $response->dynamic($session, Response::MODEL_SESSION); +}; + +App::put('/v1/account/sessions/magic-url') ->alias('/v1/account/sessions/phone') - ->desc('Create session from a token') + ->desc('Create session (deprecated)') ->label('event', 'users.[userId].sessions.[sessionId].create') ->groups(['api', 'account']) ->label('scope', 'sessions') @@ -1169,7 +1264,7 @@ App::put('/v1/account/sessions/token') ->label('usage.metric', 'sessions.{scope}.requests.create') ->label('sdk.auth', []) ->label('sdk.namespace', 'account') - ->label('sdk.method', ['createSession', 'updateMagicURLSession', 'updatePhoneSession']) + ->label('sdk.method', ['updateMagicURLSession', 'updatePhoneSession']) ->label('sdk.description', '/docs/references/account/create-session.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) @@ -1186,101 +1281,38 @@ App::put('/v1/account/sessions/token') ->inject('locale') ->inject('geodb') ->inject('queueForEvents') - ->action(function (string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents) { - $roles = Authorization::getRoles(); - $isPrivilegedUser = Auth::isPrivilegedUser($roles); - $isAppUser = Auth::isAppUser($roles); + ->action($createSession); - /** @var Utopia\Database\Document $user */ - $userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId)); - - if ($userFromRequest->isEmpty()) { - throw new Exception(Exception::USER_INVALID_TOKEN); - } - - $verifiedToken = Auth::tokenVerify($userFromRequest->getAttribute('tokens', []), null, $secret); - - if (!$verifiedToken) { - throw new Exception(Exception::USER_INVALID_TOKEN); - } - - $user->setAttributes($userFromRequest->getArrayCopy()); - - $duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $detector = new Detector($request->getUserAgent('UNKNOWN')); - $record = $geodb->get($request->getIP()); - $sessionSecret = Auth::tokenGenerator(); - $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); - - $session = new Document(array_merge( - [ - '$id' => ID::unique(), - 'userId' => $user->getId(), - 'userInternalId' => $user->getInternalId(), - 'provider' => Auth::getSessionProviderByTokenType($verifiedToken->getAttribute('type')), - 'secret' => Auth::hash($sessionSecret), // One way hash encryption to protect DB leak - 'userAgent' => $request->getUserAgent('UNKNOWN'), - 'ip' => $request->getIP(), - 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', - ], - $detector->getOS(), - $detector->getClient(), - $detector->getDevice() - )); - - Authorization::setRole(Role::user($user->getId())->toString()); - - $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())), - ])); - - $dbForProject->deleteCachedDocument('users', $user->getId()); - Authorization::skip(fn () => $dbForProject->deleteDocument('tokens', $verifiedToken->getId())); - $dbForProject->deleteCachedDocument('users', $user->getId()); - - if ($verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_MAGIC_URL) { - $user->setAttribute('emailVerification', true); - } - - if ($verifiedToken->getAttribute('type') === Auth::TOKEN_TYPE_PHONE) { - $user->setAttribute('phoneVerification', true); - } - - try { - $dbForProject->updateDocument('users', $user->getId(), $user); - } catch (\Throwable $th) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed saving user to DB'); - } - - $queueForEvents - ->setParam('userId', $user->getId()) - ->setParam('sessionId', $session->getId()); - - if (!Config::getParam('domainVerification')) { - $response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $sessionSecret)])); - } - - $protocol = $request->getProtocol(); - - $response - ->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) - ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $sessionSecret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) - ->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('expire', $expire) - ->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $sessionSecret) : '') - ; - - $response->dynamic($session, Response::MODEL_SESSION); - }); +App::post('/v1/account/sessions/token') + ->desc('Create session') + ->label('event', 'users.[userId].sessions.[sessionId].create') + ->groups(['api', 'account']) + ->label('scope', 'sessions') + ->label('auth.type', 'token') + ->label('audits.event', 'session.create') + ->label('audits.resource', 'user/{response.userId}') + ->label('audits.userId', '{response.userId}') + ->label('usage.metric', 'sessions.{scope}.requests.create') + ->label('sdk.auth', []) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'createSession') + ->label('sdk.description', '/docs/references/account/create-session.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SESSION) + ->label('abuse-limit', 10) + ->label('abuse-key', 'ip:{ip},userId:{param-userId}') + ->param('userId', '', new CustomId(), 'User ID.') + ->param('secret', '', new Text(256), 'Valid verification token.') + ->inject('request') + ->inject('response') + ->inject('user') + ->inject('dbForProject') + ->inject('project') + ->inject('locale') + ->inject('geodb') + ->inject('queueForEvents') + ->action($createSession); App::post('/v1/account/tokens/phone') ->alias('/v1/account/sessions/phone')