1
0
Fork 0
mirror of synced 2024-09-30 01:08:13 +13: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, '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' => [
[ [

View file

@ -232,7 +232,6 @@ 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)); $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
@ -248,6 +247,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(),
@ -291,7 +291,6 @@ App::post('/v1/account/sessions/email')
$session $session
->setAttribute('current', true) ->setAttribute('current', true)
->setAttribute('countryName', $countryName) ->setAttribute('countryName', $countryName)
->setAttribute('expire', $expire)
; ;
$queueForEvents $queueForEvents
@ -753,7 +752,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$record = $geodb->get($request->getIP()); $record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator(); $secret = Auth::tokenGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge([ $session = new Document(array_merge([
'$id' => ID::unique(), '$id' => ID::unique(),
'userId' => $user->getId(), 'userId' => $user->getId(),
@ -767,6 +765,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()));
if (empty($user->getAttribute('email'))) { if (empty($user->getAttribute('email'))) {
@ -793,8 +792,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$dbForProject->deleteCachedDocument('users', $user->getId()); $dbForProject->deleteCachedDocument('users', $user->getId());
$session->setAttribute('expire', $expire);
$queueForEvents $queueForEvents
->setParam('userId', $user->getId()) ->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId()) ->setParam('sessionId', $session->getId())
@ -1208,7 +1205,6 @@ App::put('/v1/account/sessions/magic-url')
$record = $geodb->get($request->getIP()); $record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator(); $secret = Auth::tokenGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge( $session = new Document(array_merge(
[ [
'$id' => ID::unique(), '$id' => ID::unique(),
@ -1219,6 +1215,7 @@ App::put('/v1/account/sessions/magic-url')
'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(),
@ -1272,8 +1269,7 @@ App::put('/v1/account/sessions/magic-url')
$session $session
->setAttribute('current', true) ->setAttribute('current', true)
->setAttribute('countryName', $countryName) ->setAttribute('countryName', $countryName);
->setAttribute('expire', $expire);
$response->dynamic($session, Response::MODEL_SESSION); $response->dynamic($session, Response::MODEL_SESSION);
}); });
@ -1488,7 +1484,6 @@ App::put('/v1/account/sessions/phone')
$record = $geodb->get($request->getIP()); $record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator(); $secret = Auth::tokenGenerator();
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration)); $expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
$session = new Document(array_merge( $session = new Document(array_merge(
[ [
'$id' => ID::unique(), '$id' => ID::unique(),
@ -1499,6 +1494,7 @@ App::put('/v1/account/sessions/phone')
'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(),
@ -1553,7 +1549,6 @@ App::put('/v1/account/sessions/phone')
$session $session
->setAttribute('current', true) ->setAttribute('current', true)
->setAttribute('countryName', $countryName) ->setAttribute('countryName', $countryName)
->setAttribute('expire', $expire)
; ;
$response->dynamic($session, Response::MODEL_SESSION); $response->dynamic($session, Response::MODEL_SESSION);
@ -1654,6 +1649,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(),
@ -1690,7 +1686,6 @@ App::post('/v1/account/sessions/anonymous')
$session $session
->setAttribute('current', true) ->setAttribute('current', true)
->setAttribute('countryName', $countryName) ->setAttribute('countryName', $countryName)
->setAttribute('expire', $expire)
; ;
$response->dynamic($session, Response::MODEL_SESSION); $response->dynamic($session, Response::MODEL_SESSION);
@ -1890,8 +1885,6 @@ App::get('/v1/account/sessions')
$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;
} }
@ -1997,7 +1990,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);
@ -2429,7 +2421,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('Renew session')
->groups(['api', 'account']) ->groups(['api', 'account'])
->label('scope', 'account') ->label('scope', 'account')
->label('event', 'users.[userId].sessions.[sessionId].update') ->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('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') ->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('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, bool $identity, Response $response, Document $user, Database $dbForProject, Document $project, Event $queueForEvents) {
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG; $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, $authDuration)
: $sessionId; : $sessionId;
$sessions = $user->getAttribute('sessions', []); $sessions = $user->getAttribute('sessions', []);
$session = null;
foreach ($sessions as $key => $session) {/** @var Document $session */ foreach ($sessions as $key => $session) {/** @var Document $session */
if ($sessionId == $session->getId()) { if ($sessionId === $session->getId()) {
// Comment below would skip re-generation if token is still valid $session = $session;
// We decided to not include this because developer can get expiration date from the session break;
// 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);
} }
} }
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') App::delete('/v1/account/sessions')
@ -2554,7 +2554,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

View file

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

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('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::addSeconds(new \DateTime($session->getAttribute('expire')), $expires)) >= DateTime::formatTz(DateTime::now())
) { ) {
return $session->getId(); return $session->getId();
} }