Merge pull request #7452 from appwrite/feat-session-renewal
Feat: session renewal
This commit is contained in:
commit
3656be3da5
9 changed files with 143 additions and 88 deletions
|
@ -681,6 +681,17 @@ $commonCollections = [
|
||||||
'array' => false,
|
'array' => false,
|
||||||
'filters' => [],
|
'filters' => [],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'$id' => ID::custom('expire'),
|
||||||
|
'type' => Database::VAR_DATETIME,
|
||||||
|
'format' => '',
|
||||||
|
'size' => 0,
|
||||||
|
'signed' => false,
|
||||||
|
'required' => true,
|
||||||
|
'default' => null,
|
||||||
|
'array' => false,
|
||||||
|
'filters' => ['datetime'],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'indexes' => [
|
'indexes' => [
|
||||||
[
|
[
|
||||||
|
|
|
@ -236,10 +236,8 @@ App::post('/v1/account/sessions/email')
|
||||||
$user->setAttributes($profile->getArrayCopy());
|
$user->setAttributes($profile->getArrayCopy());
|
||||||
|
|
||||||
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||||
|
|
||||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||||
$record = $geodb->get($request->getIP());
|
$record = $geodb->get($request->getIP());
|
||||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
|
||||||
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
|
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
|
||||||
$session = new Document(array_merge(
|
$session = new Document(array_merge(
|
||||||
[
|
[
|
||||||
|
@ -252,6 +250,7 @@ App::post('/v1/account/sessions/email')
|
||||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||||
'ip' => $request->getIP(),
|
'ip' => $request->getIP(),
|
||||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||||
|
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
|
||||||
],
|
],
|
||||||
$detector->getOS(),
|
$detector->getOS(),
|
||||||
$detector->getClient(),
|
$detector->getClient(),
|
||||||
|
@ -283,6 +282,8 @@ App::post('/v1/account/sessions/email')
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
->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'))
|
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||||
|
@ -294,7 +295,6 @@ App::post('/v1/account/sessions/email')
|
||||||
$session
|
$session
|
||||||
->setAttribute('current', true)
|
->setAttribute('current', true)
|
||||||
->setAttribute('countryName', $countryName)
|
->setAttribute('countryName', $countryName)
|
||||||
->setAttribute('expire', $expire)
|
|
||||||
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $secret) : '')
|
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $secret) : '')
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -596,8 +596,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||||
}
|
}
|
||||||
|
|
||||||
$sessions = $user->getAttribute('sessions', []);
|
$sessions = $user->getAttribute('sessions', []);
|
||||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
$current = Auth::sessionVerify($sessions, Auth::$secret);
|
||||||
$current = Auth::sessionVerify($sessions, Auth::$secret, $authDuration);
|
|
||||||
|
|
||||||
if ($current) { // Delete current session of new one.
|
if ($current) { // Delete current session of new one.
|
||||||
$currentDocument = $dbForProject->getDocument('sessions', $current);
|
$currentDocument = $dbForProject->getDocument('sessions', $current);
|
||||||
|
@ -829,6 +828,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||||
'ip' => $request->getIP(),
|
'ip' => $request->getIP(),
|
||||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||||
|
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
|
||||||
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
|
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
|
||||||
|
|
||||||
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
|
$session = $dbForProject->createDocument('sessions', $session->setAttribute('$permissions', [
|
||||||
|
@ -867,6 +867,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||||
$state['success']['query'] = URLParser::unparseQuery($query);
|
$state['success']['query'] = URLParser::unparseQuery($query);
|
||||||
$state['success'] = URLParser::unparse($state['success']);
|
$state['success'] = URLParser::unparse($state['success']);
|
||||||
|
|
||||||
|
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||||
->addHeader('Pragma', 'no-cache')
|
->addHeader('Pragma', 'no-cache')
|
||||||
|
@ -1229,7 +1231,6 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||||
$record = $geodb->get($request->getIP());
|
$record = $geodb->get($request->getIP());
|
||||||
$sessionSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
|
$sessionSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
|
||||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
|
||||||
|
|
||||||
$session = new Document(array_merge(
|
$session = new Document(array_merge(
|
||||||
[
|
[
|
||||||
|
@ -1241,6 +1242,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||||
'ip' => $request->getIP(),
|
'ip' => $request->getIP(),
|
||||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||||
|
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
|
||||||
],
|
],
|
||||||
$detector->getOS(),
|
$detector->getOS(),
|
||||||
$detector->getClient(),
|
$detector->getClient(),
|
||||||
|
@ -1282,6 +1284,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||||
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $sessionSecret)]));
|
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $sessionSecret)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||||
$protocol = $request->getProtocol();
|
$protocol = $request->getProtocol();
|
||||||
|
|
||||||
$response
|
$response
|
||||||
|
@ -1613,7 +1616,6 @@ App::post('/v1/account/sessions/anonymous')
|
||||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||||
$record = $geodb->get($request->getIP());
|
$record = $geodb->get($request->getIP());
|
||||||
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
|
$secret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
|
||||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
|
||||||
|
|
||||||
$session = new Document(array_merge(
|
$session = new Document(array_merge(
|
||||||
[
|
[
|
||||||
|
@ -1625,6 +1627,7 @@ App::post('/v1/account/sessions/anonymous')
|
||||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||||
'ip' => $request->getIP(),
|
'ip' => $request->getIP(),
|
||||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||||
|
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
|
||||||
],
|
],
|
||||||
$detector->getOS(),
|
$detector->getOS(),
|
||||||
$detector->getClient(),
|
$detector->getClient(),
|
||||||
|
@ -1650,6 +1653,8 @@ App::post('/v1/account/sessions/anonymous')
|
||||||
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
|
$response->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->addCookie(Auth::$cookieName . '_legacy', Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
->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'))
|
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), (new \DateTime($expire))->getTimestamp(), '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||||
|
@ -1661,7 +1666,6 @@ App::post('/v1/account/sessions/anonymous')
|
||||||
$session
|
$session
|
||||||
->setAttribute('current', true)
|
->setAttribute('current', true)
|
||||||
->setAttribute('countryName', $countryName)
|
->setAttribute('countryName', $countryName)
|
||||||
->setAttribute('expire', $expire)
|
|
||||||
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $secret) : '')
|
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? Auth::encodeSession($user->getId(), $secret) : '')
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -1849,16 +1853,13 @@ App::get('/v1/account/sessions')
|
||||||
->action(function (Response $response, Document $user, Locale $locale, Document $project) {
|
->action(function (Response $response, Document $user, Locale $locale, Document $project) {
|
||||||
|
|
||||||
$sessions = $user->getAttribute('sessions', []);
|
$sessions = $user->getAttribute('sessions', []);
|
||||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
$current = Auth::sessionVerify($sessions, Auth::$secret);
|
||||||
$current = Auth::sessionVerify($sessions, Auth::$secret, $authDuration);
|
|
||||||
|
|
||||||
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'));
|
$countryName = $locale->getText('countries.' . strtolower($session->getAttribute('countryCode')), $locale->getText('locale.country.unknown'));
|
||||||
|
|
||||||
$session->setAttribute('countryName', $countryName);
|
$session->setAttribute('countryName', $countryName);
|
||||||
$session->setAttribute('current', ($current == $session->getId()) ? true : false);
|
$session->setAttribute('current', ($current == $session->getId()) ? true : false);
|
||||||
$session->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)));
|
|
||||||
|
|
||||||
$sessions[$key] = $session;
|
$sessions[$key] = $session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1952,9 +1953,8 @@ App::get('/v1/account/sessions/:sessionId')
|
||||||
->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Database $dbForProject, Document $project) {
|
->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Database $dbForProject, Document $project) {
|
||||||
|
|
||||||
$sessions = $user->getAttribute('sessions', []);
|
$sessions = $user->getAttribute('sessions', []);
|
||||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
|
||||||
$sessionId = ($sessionId === 'current')
|
$sessionId = ($sessionId === 'current')
|
||||||
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret, $authDuration)
|
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
|
||||||
: $sessionId;
|
: $sessionId;
|
||||||
|
|
||||||
foreach ($sessions as $session) {/** @var Document $session */
|
foreach ($sessions as $session) {/** @var Document $session */
|
||||||
|
@ -1964,7 +1964,6 @@ App::get('/v1/account/sessions/:sessionId')
|
||||||
$session
|
$session
|
||||||
->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret)))
|
->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret)))
|
||||||
->setAttribute('countryName', $countryName)
|
->setAttribute('countryName', $countryName)
|
||||||
->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)))
|
|
||||||
;
|
;
|
||||||
|
|
||||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||||
|
@ -2346,9 +2345,8 @@ App::delete('/v1/account/sessions/:sessionId')
|
||||||
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Document $project) {
|
->action(function (?string $sessionId, ?\DateTime $requestTimestamp, Request $request, Response $response, Document $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Document $project) {
|
||||||
|
|
||||||
$protocol = $request->getProtocol();
|
$protocol = $request->getProtocol();
|
||||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
|
||||||
$sessionId = ($sessionId === 'current')
|
$sessionId = ($sessionId === 'current')
|
||||||
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret, $authDuration)
|
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
|
||||||
: $sessionId;
|
: $sessionId;
|
||||||
|
|
||||||
$sessions = $user->getAttribute('sessions', []);
|
$sessions = $user->getAttribute('sessions', []);
|
||||||
|
@ -2396,7 +2394,7 @@ App::delete('/v1/account/sessions/:sessionId')
|
||||||
});
|
});
|
||||||
|
|
||||||
App::patch('/v1/account/sessions/:sessionId')
|
App::patch('/v1/account/sessions/:sessionId')
|
||||||
->desc('Update OAuth session (refresh tokens)')
|
->desc('Update (or renew) a session')
|
||||||
->groups(['api', 'account'])
|
->groups(['api', 'account'])
|
||||||
->label('scope', 'accounts.write')
|
->label('scope', 'accounts.write')
|
||||||
->label('event', 'users.[userId].sessions.[sessionId].update')
|
->label('event', 'users.[userId].sessions.[sessionId].update')
|
||||||
|
@ -2413,61 +2411,56 @@ App::patch('/v1/account/sessions/:sessionId')
|
||||||
->label('sdk.response.model', Response::MODEL_SESSION)
|
->label('sdk.response.model', Response::MODEL_SESSION)
|
||||||
->label('abuse-limit', 10)
|
->label('abuse-limit', 10)
|
||||||
->param('sessionId', '', new UID(), 'Session ID. Use the string \'current\' to update the current device session.')
|
->param('sessionId', '', new UID(), 'Session ID. Use the string \'current\' to update the current device session.')
|
||||||
->inject('request')
|
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('user')
|
->inject('user')
|
||||||
->inject('dbForProject')
|
->inject('dbForProject')
|
||||||
->inject('project')
|
->inject('project')
|
||||||
->inject('locale')
|
|
||||||
->inject('queueForEvents')
|
->inject('queueForEvents')
|
||||||
->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Event $queueForEvents) {
|
->action(function (?string $sessionId, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents) {
|
||||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
|
||||||
$sessionId = ($sessionId === 'current')
|
|
||||||
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret, $authDuration)
|
|
||||||
: $sessionId;
|
|
||||||
|
|
||||||
|
$sessionId = ($sessionId === 'current')
|
||||||
|
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
|
||||||
|
: $sessionId;
|
||||||
$sessions = $user->getAttribute('sessions', []);
|
$sessions = $user->getAttribute('sessions', []);
|
||||||
|
|
||||||
foreach ($sessions as $key => $session) {/** @var Document $session */
|
$session = null;
|
||||||
if ($sessionId == $session->getId()) {
|
foreach ($sessions as $key => $value) {
|
||||||
// Comment below would skip re-generation if token is still valid
|
if ($sessionId === $value->getId()) {
|
||||||
// We decided to not include this because developer can get expiration date from the session
|
$session = $value;
|
||||||
// I kept code in comment because it might become relevant in the future
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// $expireAt = (int) $session->getAttribute('providerAccessTokenExpiry');
|
if ($session === null) {
|
||||||
// if(\time() < $expireAt - 5) { // 5 seconds time-sync and networking gap, to be safe
|
throw new Exception(Exception::USER_SESSION_NOT_FOUND);
|
||||||
// return $response->noContent();
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
$provider = $session->getAttribute('provider');
|
// Extend session
|
||||||
$refreshToken = $session->getAttribute('providerRefreshToken');
|
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||||
|
$session->setAttribute('expire', DateTime::addSeconds(new \DateTime(), $authDuration));
|
||||||
|
|
||||||
|
// Refresh OAuth access token
|
||||||
|
$provider = $session->getAttribute('provider', '');
|
||||||
|
$refreshToken = $session->getAttribute('providerRefreshToken', '');
|
||||||
|
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
|
||||||
|
|
||||||
|
if (!empty($provider) && \class_exists($className)) {
|
||||||
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
|
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
|
||||||
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
|
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
|
||||||
|
|
||||||
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
|
|
||||||
|
|
||||||
if (!\class_exists($className)) {
|
|
||||||
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
$oauth2 = new $className($appId, $appSecret, '', [], []);
|
$oauth2 = new $className($appId, $appSecret, '', [], []);
|
||||||
|
|
||||||
$oauth2->refreshTokens($refreshToken);
|
$oauth2->refreshTokens($refreshToken);
|
||||||
|
|
||||||
$session
|
$session
|
||||||
->setAttribute('providerAccessToken', $oauth2->getAccessToken(''))
|
->setAttribute('providerAccessToken', $oauth2->getAccessToken(''))
|
||||||
->setAttribute('providerRefreshToken', $oauth2->getRefreshToken(''))
|
->setAttribute('providerRefreshToken', $oauth2->getRefreshToken(''))
|
||||||
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
|
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes
|
||||||
$dbForProject->updateDocument('sessions', $sessionId, $session);
|
$dbForProject->updateDocument('sessions', $sessionId, $session);
|
||||||
|
|
||||||
$dbForProject->deleteCachedDocument('users', $user->getId());
|
$dbForProject->deleteCachedDocument('users', $user->getId());
|
||||||
|
|
||||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
|
||||||
|
|
||||||
$session->setAttribute('expire', DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $authDuration)));
|
|
||||||
|
|
||||||
$queueForEvents
|
$queueForEvents
|
||||||
->setParam('userId', $user->getId())
|
->setParam('userId', $user->getId())
|
||||||
->setParam('sessionId', $session->getId())
|
->setParam('sessionId', $session->getId())
|
||||||
|
@ -2475,10 +2468,6 @@ App::patch('/v1/account/sessions/:sessionId')
|
||||||
;
|
;
|
||||||
|
|
||||||
return $response->dynamic($session, Response::MODEL_SESSION);
|
return $response->dynamic($session, Response::MODEL_SESSION);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Exception(Exception::USER_SESSION_NOT_FOUND);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
App::delete('/v1/account/sessions')
|
App::delete('/v1/account/sessions')
|
||||||
|
@ -2521,7 +2510,6 @@ App::delete('/v1/account/sessions')
|
||||||
|
|
||||||
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) {
|
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) {
|
||||||
$session->setAttribute('current', true);
|
$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
|
// If current session delete the cookies too
|
||||||
$response
|
$response
|
||||||
|
|
|
@ -962,6 +962,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||||
'ip' => $request->getIP(),
|
'ip' => $request->getIP(),
|
||||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||||
|
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
|
||||||
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
|
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
|
||||||
|
|
||||||
$session = $dbForProject->createDocument('sessions', $session
|
$session = $dbForProject->createDocument('sessions', $session
|
||||||
|
|
|
@ -1108,11 +1108,9 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
|
||||||
Authorization::setDefaultStatus(true);
|
Authorization::setDefaultStatus(true);
|
||||||
|
|
||||||
Auth::setCookieName('a_session_' . $project->getId());
|
Auth::setCookieName('a_session_' . $project->getId());
|
||||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
|
||||||
|
|
||||||
if (APP_MODE_ADMIN === $mode) {
|
if (APP_MODE_ADMIN === $mode) {
|
||||||
Auth::setCookieName('a_session_' . $console->getId());
|
Auth::setCookieName('a_session_' . $console->getId());
|
||||||
$authDuration = Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$session = Auth::decodeSession(
|
$session = Auth::decodeSession(
|
||||||
|
@ -1164,7 +1162,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons
|
||||||
|
|
||||||
if (
|
if (
|
||||||
$user->isEmpty() // Check a document has been found in the DB
|
$user->isEmpty() // Check a document has been found in the DB
|
||||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret, $authDuration)
|
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)
|
||||||
) { // Validate user has valid login token
|
) { // Validate user has valid login token
|
||||||
$user = new Document([]);
|
$user = new Document([]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -545,11 +545,10 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
|
||||||
Auth::$secret = $session['secret'] ?? '';
|
Auth::$secret = $session['secret'] ?? '';
|
||||||
|
|
||||||
$user = $database->getDocument('users', Auth::$unique);
|
$user = $database->getDocument('users', Auth::$unique);
|
||||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
empty($user->getId()) // Check a document has been found in the DB
|
empty($user->getId()) // Check a document has been found in the DB
|
||||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret, $authDuration) // Validate user has valid login token
|
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret) // Validate user has valid login token
|
||||||
) {
|
) {
|
||||||
// cookie not valid
|
// cookie not valid
|
||||||
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
|
throw new Exception(Exception::REALTIME_MESSAGE_FORMAT_INVALID, 'Session is not valid.');
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Access tokens have limited lifespan and expire to mitigate security risks. If session was created using an OAuth provider, this route can be used to "refresh" the access token.
|
Extend session's expiry to increase it's lifespan. Extending a session is useful when session length is short such as 5 minutes.
|
|
@ -375,11 +375,10 @@ class Auth
|
||||||
*
|
*
|
||||||
* @param array $sessions
|
* @param array $sessions
|
||||||
* @param string $secret
|
* @param string $secret
|
||||||
* @param string $expires
|
|
||||||
*
|
*
|
||||||
* @return bool|string
|
* @return bool|string
|
||||||
*/
|
*/
|
||||||
public static function sessionVerify(array $sessions, string $secret, int $expires)
|
public static function sessionVerify(array $sessions, string $secret)
|
||||||
{
|
{
|
||||||
foreach ($sessions as $session) {
|
foreach ($sessions as $session) {
|
||||||
/** @var Document $session */
|
/** @var Document $session */
|
||||||
|
@ -387,7 +386,7 @@ class Auth
|
||||||
$session->isSet('secret') &&
|
$session->isSet('secret') &&
|
||||||
$session->isSet('provider') &&
|
$session->isSet('provider') &&
|
||||||
$session->getAttribute('secret') === self::hash($secret) &&
|
$session->getAttribute('secret') === self::hash($secret) &&
|
||||||
DateTime::formatTz(DateTime::addSeconds(new \DateTime($session->getCreatedAt()), $expires)) >= DateTime::formatTz(DateTime::now())
|
DateTime::formatTz(DateTime::format(new \DateTime($session->getAttribute('expire')))) >= DateTime::formatTz(DateTime::now())
|
||||||
) {
|
) {
|
||||||
return $session->getId();
|
return $session->getId();
|
||||||
}
|
}
|
||||||
|
|
|
@ -691,7 +691,7 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
'content-type' => 'application/json',
|
'content-type' => 'application/json',
|
||||||
'x-appwrite-project' => $this->getProject()['$id'],
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
], $this->getHeaders()), [
|
], $this->getHeaders()), [
|
||||||
'duration' => 60, // Set session duration to 2 minutes
|
'duration' => 60, // Set session duration to 1 minute
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals(200, $response['headers']['status-code']);
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
@ -765,6 +765,61 @@ class ProjectsConsoleClientTest extends Scope
|
||||||
|
|
||||||
$this->assertEquals(401, $response['headers']['status-code']);
|
$this->assertEquals(401, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
// Set session duration to 15s
|
||||||
|
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/duration', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $this->getProject()['$id'],
|
||||||
|
], $this->getHeaders()), [
|
||||||
|
'duration' => 15, // seconds
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
$this->assertEquals(15, $response['body']['authDuration']);
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $projectId,
|
||||||
|
]), [
|
||||||
|
'email' => $userEmail,
|
||||||
|
'password' => 'password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertEquals(201, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
$sessionCookie = $response['headers']['set-cookie'];
|
||||||
|
|
||||||
|
// Wait 10 seconds, ensure valid session, extend session
|
||||||
|
\sleep(10);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $projectId,
|
||||||
|
'Cookie' => $sessionCookie,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_PATCH, '/account/sessions/current', array_merge([
|
||||||
|
'origin' => 'http://localhost',
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $projectId,
|
||||||
|
'cookie' => $sessionCookie,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response['headers']['status-code']);
|
||||||
|
|
||||||
|
// Wait 20 seconds, ensure non-valid session
|
||||||
|
\sleep(20);
|
||||||
|
|
||||||
|
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
|
||||||
|
'content-type' => 'application/json',
|
||||||
|
'x-appwrite-project' => $projectId,
|
||||||
|
'Cookie' => $sessionCookie,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertEquals(401, $response['headers']['status-code']);
|
||||||
|
|
||||||
// Return project back to normal
|
// Return project back to normal
|
||||||
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/duration', array_merge([
|
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/duration', array_merge([
|
||||||
'content-type' => 'application/json',
|
'content-type' => 'application/json',
|
||||||
|
|
|
@ -213,12 +213,14 @@ class AuthTest extends TestCase
|
||||||
'secret' => $hash,
|
'secret' => $hash,
|
||||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||||
'providerUid' => 'test@example.com',
|
'providerUid' => 'test@example.com',
|
||||||
|
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
|
||||||
]),
|
]),
|
||||||
new Document([
|
new Document([
|
||||||
'$id' => ID::custom('token2'),
|
'$id' => ID::custom('token2'),
|
||||||
'secret' => 'secret2',
|
'secret' => 'secret2',
|
||||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||||
'providerUid' => 'test@example.com',
|
'providerUid' => 'test@example.com',
|
||||||
|
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime1),
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -230,19 +232,21 @@ class AuthTest extends TestCase
|
||||||
'secret' => $hash,
|
'secret' => $hash,
|
||||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||||
'providerUid' => 'test@example.com',
|
'providerUid' => 'test@example.com',
|
||||||
|
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
|
||||||
]),
|
]),
|
||||||
new Document([
|
new Document([
|
||||||
'$id' => ID::custom('token2'),
|
'$id' => ID::custom('token2'),
|
||||||
'secret' => 'secret2',
|
'secret' => 'secret2',
|
||||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||||
'providerUid' => 'test@example.com',
|
'providerUid' => 'test@example.com',
|
||||||
|
'expire' => DateTime::addSeconds(new \DateTime(), $expireTime2),
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->assertEquals(Auth::sessionVerify($tokens1, $secret, $expireTime1), 'token1');
|
$this->assertEquals(Auth::sessionVerify($tokens1, $secret), 'token1');
|
||||||
$this->assertEquals(Auth::sessionVerify($tokens1, 'false-secret', $expireTime1), false);
|
$this->assertEquals(Auth::sessionVerify($tokens1, 'false-secret'), false);
|
||||||
$this->assertEquals(Auth::sessionVerify($tokens2, $secret, $expireTime2), false);
|
$this->assertEquals(Auth::sessionVerify($tokens2, $secret), false);
|
||||||
$this->assertEquals(Auth::sessionVerify($tokens2, 'false-secret', $expireTime2), false);
|
$this->assertEquals(Auth::sessionVerify($tokens2, 'false-secret'), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTokenVerify(): void
|
public function testTokenVerify(): void
|
||||||
|
|
Loading…
Reference in a new issue