diff --git a/app/config/collections.php b/app/config/collections.php index cbaed36f71..ef4e0a6365 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -681,6 +681,17 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('expire'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ], ], 'indexes' => [ [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 35aa0dfecd..b6851fe722 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -247,6 +247,7 @@ App::post('/v1/account/sessions/email') 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', + 'expire' => $expire ], $detector->getOS(), $detector->getClient(), @@ -290,7 +291,6 @@ App::post('/v1/account/sessions/email') $session ->setAttribute('current', true) ->setAttribute('countryName', $countryName) - ->setAttribute('expire', $expire) ; $queueForEvents @@ -758,6 +758,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', + 'expire' => $expire ], $detector->getOS(), $detector->getClient(), $detector->getDevice())); if (empty($user->getAttribute('email'))) { @@ -784,8 +785,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $dbForProject->deleteCachedDocument('users', $user->getId()); - $session->setAttribute('expire', $expire); - $queueForEvents ->setParam('userId', $user->getId()) ->setParam('sessionId', $session->getId()) @@ -1176,6 +1175,7 @@ App::put('/v1/account/sessions/magic-url') 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', + 'expire' => $expire ], $detector->getOS(), $detector->getClient(), @@ -1229,8 +1229,7 @@ App::put('/v1/account/sessions/magic-url') $session ->setAttribute('current', true) - ->setAttribute('countryName', $countryName) - ->setAttribute('expire', $expire); + ->setAttribute('countryName', $countryName); $response->dynamic($session, Response::MODEL_SESSION); }); @@ -1451,6 +1450,7 @@ App::put('/v1/account/sessions/phone') 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', + 'expire' => $expire ], $detector->getOS(), $detector->getClient(), @@ -1505,7 +1505,6 @@ App::put('/v1/account/sessions/phone') $session ->setAttribute('current', true) ->setAttribute('countryName', $countryName) - ->setAttribute('expire', $expire) ; $response->dynamic($session, Response::MODEL_SESSION); @@ -1606,6 +1605,7 @@ App::post('/v1/account/sessions/anonymous') 'userAgent' => $request->getUserAgent('UNKNOWN'), 'ip' => $request->getIP(), 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', + 'expire' => $expire ], $detector->getOS(), $detector->getClient(), @@ -1642,7 +1642,6 @@ App::post('/v1/account/sessions/anonymous') $session ->setAttribute('current', true) ->setAttribute('countryName', $countryName) - ->setAttribute('expire', $expire) ; $response->dynamic($session, Response::MODEL_SESSION); @@ -1842,7 +1841,6 @@ App::get('/v1/account/sessions') $session->setAttribute('countryName', $countryName); $session->setAttribute('current', ($current == $session->getId()) ? true : false); - $session->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration))); $sessions[$key] = $session; } @@ -1949,7 +1947,6 @@ App::get('/v1/account/sessions/:sessionId') $session ->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret))) ->setAttribute('countryName', $countryName) - ->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration))) ; return $response->dynamic($session, Response::MODEL_SESSION); @@ -2451,8 +2448,6 @@ App::patch('/v1/account/sessions/:sessionId') $authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; - $session->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration))); - $queueForEvents ->setParam('userId', $user->getId()) ->setParam('sessionId', $session->getId()) @@ -2515,7 +2510,6 @@ App::delete('/v1/account/sessions') if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { $session->setAttribute('current', true); - $session->setAttribute('expire', DateTime::addSeconds(new \DateTime($session->getCreatedAt()), Auth::TOKEN_EXPIRATION_LOGIN_LONG)); // If current session delete the cookies too $response diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index fe441e0e8c..300d9c180e 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -693,6 +693,37 @@ App::patch('/v1/projects/:projectId/auth/duration') $response->dynamic($project, Response::MODEL_PROJECT); }); +App::patch('/v1/projects/:projectId/auth/session-refresh') + ->desc('Update project session refresh') + ->groups(['api', 'projects']) + ->label('scope', 'projects.write') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.namespace', 'projects') + ->label('sdk.method', 'updateSessionRefresh') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_PROJECT) + ->param('projectId', '', new UID(), 'Project unique ID.') + ->param('sessionRefresh', false, new Boolean(), 'Automatic session refresh. If enabled, sessions are automatically extended to session duration on every request.', true) + ->inject('response') + ->inject('dbForConsole') + ->action(function (string $projectId, int $sessionRefresh, Response $response, Database $dbForConsole) { + + $project = $dbForConsole->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $auths = $project->getAttribute('auths', []); + $auths['sessionRefresh'] = $sessionRefresh; + + $dbForConsole->updateDocument('projects', $project->getId(), $project + ->setAttribute('auths', $auths)); + + $response->dynamic($project, Response::MODEL_PROJECT); + }); + App::patch('/v1/projects/:projectId/auth/:method') ->desc('Update project auth method status. Use this endpoint to enable or disable a given auth method for this project.') ->groups(['api', 'projects']) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index b37d76a816..4164475286 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -154,8 +154,7 @@ App::init() $response ->addHeader('X-RateLimit-Limit', $limit) ->addHeader('X-RateLimit-Remaining', $remaining) - ->addHeader('X-RateLimit-Reset', $time) - ; + ->addHeader('X-RateLimit-Reset', $time); } $enabled = App::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; @@ -170,6 +169,27 @@ App::init() } } + /* + * Session refresh + */ + if ($project->getAttribute('auths', [])['sessionRefresh'] ?? false) { + if ($user && !$user->isEmpty()) { + $authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $currentSessionId = Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret, $authDuration); + + if ($currentSessionId) { + $currentSession = $dbForProject->getDocument('sessions', $currentSessionId); + if (!$currentSession->isEmpty()) { + $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $authDuration)); + $currentSession = $currentSession->setAttribute('expire', $expire); + + $dbForProject->updateDocument('sessions', $currentSession->getId(), $currentSession); + $dbForProject->deleteCachedDocument('users', $user->getId()); + } + } + } + } + /* * Background Jobs */ @@ -240,7 +260,7 @@ App::init() if ($fileSecurity && !$valid) { $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } else { - $file = Authorization::skip(fn() => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); } if ($file->isEmpty()) { @@ -252,8 +272,7 @@ App::init() ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT') ->addHeader('X-Appwrite-Cache', 'hit') ->setContentType($data['contentType']) - ->send(base64_decode($data['payload'])) - ; + ->send(base64_decode($data['payload'])); } else { $response->addHeader('X-Appwrite-Cache', 'miss'); } @@ -495,7 +514,7 @@ App::shutdown() 'resource' => $resource, 'contentType' => $response->getContentType(), 'payload' => base64_encode($data['payload']), - ]) ; + ]); $signature = md5($data); $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); @@ -503,10 +522,10 @@ App::shutdown() $now = DateTime::now(); if ($cacheLog->isEmpty()) { Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([ - '$id' => $key, - 'resource' => $resource, - 'accessedAt' => $now, - 'signature' => $signature, + '$id' => $key, + 'resource' => $resource, + 'accessedAt' => $now, + 'signature' => $signature, ]))); } elseif (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_CACHE_UPDATE)) > $accessedAt) { $cacheLog->setAttribute('accessedAt', $now); diff --git a/src/Appwrite/Auth/Auth.php b/src/Appwrite/Auth/Auth.php index e97c271ae2..45bd9957c4 100644 --- a/src/Appwrite/Auth/Auth.php +++ b/src/Appwrite/Auth/Auth.php @@ -363,7 +363,7 @@ class Auth $session->isSet('secret') && $session->isSet('provider') && $session->getAttribute('secret') === self::hash($secret) && - DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $expires)) >= DateTime::formatTz(DateTime::now()) + DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getAttribute('expire')), $expires)) >= DateTime::formatTz(DateTime::now()) ) { return $session->getId(); }