1
0
Fork 0
mirror of synced 2024-06-28 19:20:25 +12:00

Implement session renewal

This commit is contained in:
Matej Bačo 2024-01-15 13:43:21 +00:00
parent e9de0332cc
commit 4f5755e7d0
5 changed files with 80 additions and 69 deletions

View file

@ -681,6 +681,17 @@ $commonCollections = [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('expire'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[

View file

@ -232,7 +232,6 @@ App::post('/v1/account/sessions/email')
$user->setAttributes($profile->getArrayCopy());
$duration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
@ -248,6 +247,7 @@ App::post('/v1/account/sessions/email')
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
],
$detector->getOS(),
$detector->getClient(),
@ -291,7 +291,6 @@ App::post('/v1/account/sessions/email')
$session
->setAttribute('current', true)
->setAttribute('countryName', $countryName)
->setAttribute('expire', $expire)
;
$queueForEvents
@ -753,7 +752,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge([
'$id' => ID::unique(),
'userId' => $user->getId(),
@ -767,6 +765,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
if (empty($user->getAttribute('email'))) {
@ -793,8 +792,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())
@ -1208,7 +1205,6 @@ App::put('/v1/account/sessions/magic-url')
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
[
'$id' => ID::unique(),
@ -1219,6 +1215,7 @@ App::put('/v1/account/sessions/magic-url')
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
],
$detector->getOS(),
$detector->getClient(),
@ -1272,8 +1269,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);
});
@ -1488,7 +1484,6 @@ App::put('/v1/account/sessions/phone')
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge(
[
'$id' => ID::unique(),
@ -1499,6 +1494,7 @@ App::put('/v1/account/sessions/phone')
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
],
$detector->getOS(),
$detector->getClient(),
@ -1553,7 +1549,6 @@ App::put('/v1/account/sessions/phone')
$session
->setAttribute('current', true)
->setAttribute('countryName', $countryName)
->setAttribute('expire', $expire)
;
$response->dynamic($session, Response::MODEL_SESSION);
@ -1654,6 +1649,7 @@ App::post('/v1/account/sessions/anonymous')
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
],
$detector->getOS(),
$detector->getClient(),
@ -1690,7 +1686,6 @@ App::post('/v1/account/sessions/anonymous')
$session
->setAttribute('current', true)
->setAttribute('countryName', $countryName)
->setAttribute('expire', $expire)
;
$response->dynamic($session, Response::MODEL_SESSION);
@ -1890,8 +1885,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;
}
@ -1997,7 +1990,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);
@ -2429,7 +2421,7 @@ App::delete('/v1/account/sessions/:sessionId')
});
App::patch('/v1/account/sessions/:sessionId')
->desc('Update OAuth session (refresh tokens)')
->desc('Renew session')
->groups(['api', 'account'])
->label('scope', 'account')
->label('event', 'users.[userId].sessions.[sessionId].update')
@ -2446,72 +2438,80 @@ App::patch('/v1/account/sessions/:sessionId')
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->param('sessionId', '', new UID(), 'Session ID. Use the string \'current\' to update the current device session.')
->inject('request')
->param('identity', false, new Boolean(), 'Refresh OAuth2 identity. If enabled and session was created using an OAuth provider, the access token of the identity will be refreshed', true)
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('project')
->inject('locale')
->inject('queueForEvents')
->action(function (?string $sessionId, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Locale $locale, Event $queueForEvents) {
->action(function (?string $sessionId, bool $identity, 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;
$sessions = $user->getAttribute('sessions', []);
$session = null;
foreach ($sessions as $key => $session) {/** @var Document $session */
if ($sessionId == $session->getId()) {
// Comment below would skip re-generation if token is still valid
// We decided to not include this because developer can get expiration date from the session
// I kept code in comment because it might become relevant in the future
// $expireAt = (int) $session->getAttribute('providerAccessTokenExpiry');
// if(\time() < $expireAt - 5) { // 5 seconds time-sync and networking gap, to be safe
// return $response->noContent();
// }
$provider = $session->getAttribute('provider');
$refreshToken = $session->getAttribute('providerRefreshToken');
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$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->refreshTokens($refreshToken);
$session
->setAttribute('providerAccessToken', $oauth2->getAccessToken(''))
->setAttribute('providerRefreshToken', $oauth2->getRefreshToken(''))
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
$dbForProject->updateDocument('sessions', $sessionId, $session);
$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
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION))
;
return $response->dynamic($session, Response::MODEL_SESSION);
if ($sessionId === $session->getId()) {
$session = $session;
break;
}
}
throw new Exception(Exception::USER_SESSION_NOT_FOUND);
if ($session === null) {
throw new Exception(Exception::USER_SESSION_NOT_FOUND);
}
// Extend session
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$session->setAttribute('expire', DateTime::addSeconds(new \DateTime(), $authDuration));
// Refresh OAuth access token
if ($identity) {
// Comment below would skip re-generation if token is still valid
// We decided to not include this because developer can get expiration date from the session
// I kept code in comment because it might become relevant in the future
// $expireAt = (int) $session->getAttribute('providerAccessTokenExpiry');
// if(\time() < $expireAt - 5) { // 5 seconds time-sync and networking gap, to be safe
// return $response->noContent();
// }
$provider = $session->getAttribute('provider');
$refreshToken = $session->getAttribute('providerRefreshToken');
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$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->refreshTokens($refreshToken);
$session
->setAttribute('providerAccessToken', $oauth2->getAccessToken(''))
->setAttribute('providerRefreshToken', $oauth2->getRefreshToken(''))
->setAttribute('providerAccessTokenExpiry', DateTime::addSeconds(new \DateTime(), (int)$oauth2->getAccessTokenExpiry('')));
}
$dbForProject->updateDocument('sessions', $sessionId, $session);
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId())
->setPayload($response->output($session, Response::MODEL_SESSION))
;
return $response->dynamic($session, Response::MODEL_SESSION);
});
App::delete('/v1/account/sessions')
@ -2554,7 +2554,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

View file

@ -962,6 +962,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$session = $dbForProject->createDocument('sessions', $session

View file

@ -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.

View file

@ -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();
}