mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 08:58:35 +00:00
Merge pull request #922 from TorstenDittmann/refactor-user-tokens
refactor-user-tokens
This commit is contained in:
commit
cd6263e8b7
13 changed files with 467 additions and 112 deletions
|
|
@ -271,6 +271,16 @@ $collections = [
|
|||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'Sessions',
|
||||
'key' => 'sessions',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_DOCUMENT,
|
||||
'default' => [],
|
||||
'required' => false,
|
||||
'array' => true,
|
||||
'list' => [Database::SYSTEM_COLLECTION_SESSIONS],
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'Tokens',
|
||||
|
|
@ -293,11 +303,11 @@ $collections = [
|
|||
],
|
||||
],
|
||||
],
|
||||
Database::SYSTEM_COLLECTION_TOKENS => [
|
||||
Database::SYSTEM_COLLECTION_SESSIONS => [
|
||||
'$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,
|
||||
'$id' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$id' => Database::SYSTEM_COLLECTION_SESSIONS,
|
||||
'$permissions' => ['read' => ['*']],
|
||||
'name' => 'Token',
|
||||
'name' => 'Session',
|
||||
'structure' => true,
|
||||
'rules' => [
|
||||
[
|
||||
|
|
@ -306,16 +316,34 @@ $collections = [
|
|||
'key' => 'userId',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => null,
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'Provider',
|
||||
'key' => 'provider',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => '',
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'Provider User Identifier',
|
||||
'key' => 'providerUid',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'Type',
|
||||
'key' => 'type',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_NUMERIC,
|
||||
'default' => null,
|
||||
'required' => true,
|
||||
'label' => 'Provider Token',
|
||||
'key' => 'providerToken',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
|
|
@ -473,6 +501,69 @@ $collections = [
|
|||
],
|
||||
],
|
||||
],
|
||||
Database::SYSTEM_COLLECTION_TOKENS => [
|
||||
'$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,
|
||||
'$id' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$permissions' => ['read' => ['*']],
|
||||
'name' => 'Token',
|
||||
'structure' => true,
|
||||
'rules' => [
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'User ID',
|
||||
'key' => 'userId',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => null,
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'Type',
|
||||
'key' => 'type',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_NUMERIC,
|
||||
'default' => null,
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'Secret',
|
||||
'key' => 'secret',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => '',
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'Expire',
|
||||
'key' => 'expire',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_NUMERIC,
|
||||
'default' => 0,
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'User Agent',
|
||||
'key' => 'userAgent',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => '',
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'IP',
|
||||
'key' => 'ip',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_IP,
|
||||
'default' => '',
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
Database::SYSTEM_COLLECTION_MEMBERSHIPS => [
|
||||
'$collection' => Database::SYSTEM_COLLECTION_COLLECTIONS,
|
||||
'$id' => Database::SYSTEM_COLLECTION_MEMBERSHIPS,
|
||||
|
|
@ -1617,26 +1708,6 @@ foreach ($providers as $index => $provider) {
|
|||
'array' => false,
|
||||
'filter' => ['encrypt'],
|
||||
];
|
||||
|
||||
$collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'OAuth2 '.\ucfirst($index).' ID',
|
||||
'key' => 'oauth2'.\ucfirst($index),
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
];
|
||||
|
||||
$collections[Database::SYSTEM_COLLECTION_USERS]['rules'][] = [
|
||||
'$collection' => Database::SYSTEM_COLLECTION_RULES,
|
||||
'label' => 'OAuth2 '.\ucfirst($index).' Access Token',
|
||||
'key' => 'oauth2'.\ucfirst($index).'AccessToken',
|
||||
'type' => Database::SYSTEM_VAR_TYPE_TEXT,
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $collections;
|
||||
|
|
@ -190,10 +190,11 @@ App::post('/v1/account/sessions')
|
|||
$secret = Auth::tokenGenerator();
|
||||
$session = new Document(array_merge(
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$collection' => Database::SYSTEM_COLLECTION_SESSIONS,
|
||||
'$permissions' => ['read' => ['user:'.$profile->getId()], 'write' => ['user:'.$profile->getId()]],
|
||||
'userId' => $profile->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => $email,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expiry,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
|
|
@ -210,7 +211,7 @@ App::post('/v1/account/sessions')
|
|||
throw new Exception('Failed saving session to DB', 500);
|
||||
}
|
||||
|
||||
$profile->setAttribute('tokens', $session, Document::SET_TYPE_APPEND);
|
||||
$profile->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
|
||||
|
||||
$profile = $projectDB->updateDocument($profile->getArrayCopy());
|
||||
|
||||
|
|
@ -441,7 +442,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
throw new Exception('Missing ID from OAuth2 provider', 400);
|
||||
}
|
||||
|
||||
$current = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret);
|
||||
$current = Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret);
|
||||
|
||||
if ($current) {
|
||||
$projectDB->deleteDocument($current); //throw new Exception('User already logged in', 401);
|
||||
|
|
@ -451,7 +452,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
'limit' => 1,
|
||||
'filters' => [
|
||||
'$collection='.Database::SYSTEM_COLLECTION_USERS,
|
||||
'oauth2'.\ucfirst($provider).'='.$oauth2ID,
|
||||
'sessions.provider='.$provider,
|
||||
'sessions.providerUid='.$oauth2ID
|
||||
],
|
||||
]) : $user;
|
||||
|
||||
|
|
@ -506,10 +508,12 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
$secret = Auth::tokenGenerator();
|
||||
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$session = new Document(array_merge([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$collection' => Database::SYSTEM_COLLECTION_SESSIONS,
|
||||
'$permissions' => ['read' => ['user:'.$user['$id']], 'write' => ['user:'.$user['$id']]],
|
||||
'userId' => $user->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'provider' => $provider,
|
||||
'providerUid' => $oauth2ID,
|
||||
'providerToken' => $accessToken,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expiry,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
|
|
@ -527,10 +531,8 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
|||
}
|
||||
|
||||
$user
|
||||
->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID)
|
||||
->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken)
|
||||
->setAttribute('status', Auth::USER_STATUS_ACTIVATED)
|
||||
->setAttribute('tokens', $session, Document::SET_TYPE_APPEND)
|
||||
->setAttribute('sessions', $session, Document::SET_TYPE_APPEND)
|
||||
;
|
||||
|
||||
Authorization::setRole('user:'.$user->getId());
|
||||
|
|
@ -648,10 +650,10 @@ App::post('/v1/account/sessions/anonymous')
|
|||
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$session = new Document(array_merge(
|
||||
[
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$collection' => Database::SYSTEM_COLLECTION_SESSIONS,
|
||||
'$permissions' => ['read' => ['user:' . $user['$id']], 'write' => ['user:' . $user['$id']]],
|
||||
'userId' => $user->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'provider' => Auth::SESSION_PROVIDER_ANONYMOUS,
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expiry,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
|
|
@ -663,7 +665,7 @@ App::post('/v1/account/sessions/anonymous')
|
|||
$detector->getDevice()
|
||||
));
|
||||
|
||||
$user->setAttribute('tokens', $session, Document::SET_TYPE_APPEND);
|
||||
$user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
|
||||
|
||||
Authorization::setRole('user:'.$user->getId());
|
||||
|
||||
|
|
@ -716,16 +718,18 @@ App::post('/v1/account/jwt')
|
|||
/** @var Appwrite\Utopia\Response $response */
|
||||
/** @var Appwrite\Database\Document $user */
|
||||
|
||||
$tokens = $user->getAttribute('tokens', []);
|
||||
$session = new Document();
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$current = new Document();
|
||||
|
||||
foreach ($tokens as $token) { /** @var Appwrite\Database\Document $token */
|
||||
if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
|
||||
$session = $token;
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Appwrite\Database\Document $session */
|
||||
|
||||
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
|
||||
$current = $session;
|
||||
}
|
||||
}
|
||||
|
||||
if($session->isEmpty()) {
|
||||
if($current->isEmpty()) {
|
||||
throw new Exception('No valid session found', 401);
|
||||
}
|
||||
|
||||
|
|
@ -739,7 +743,7 @@ App::post('/v1/account/jwt')
|
|||
// 'scopes' => ['user'],
|
||||
// 'iss' => 'http://api.mysite.com',
|
||||
'userId' => $user->getId(),
|
||||
'sessionId' => $session->getId(),
|
||||
'sessionId' => $current->getId(),
|
||||
])]), Response::MODEL_JWT);
|
||||
});
|
||||
|
||||
|
|
@ -804,22 +808,19 @@ App::get('/v1/account/sessions')
|
|||
/** @var Appwrite\Database\Document $user */
|
||||
/** @var Utopia\Locale\Locale $locale */
|
||||
|
||||
$tokens = $user->getAttribute('tokens', []);
|
||||
$sessions = [];
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$countries = $locale->getText('countries');
|
||||
$current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret);
|
||||
$current = Auth::sessionVerify($sessions, Auth::$secret);
|
||||
|
||||
foreach ($tokens as $token) { /* @var $token Document */
|
||||
if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) {
|
||||
continue;
|
||||
}
|
||||
foreach ($sessions as $key => $session) {
|
||||
/** @var Document $session */
|
||||
|
||||
$token->setAttribute('countryName', (isset($countries[strtoupper($token->getAttribute('countryCode'))]))
|
||||
? $countries[strtoupper($token->getAttribute('countryCode'))]
|
||||
$session->setAttribute('countryName', (isset($countries[strtoupper($session->getAttribute('countryCode'))]))
|
||||
? $countries[strtoupper($session->getAttribute('countryCode'))]
|
||||
: $locale->getText('locale.country.unknown'));
|
||||
$token->setAttribute('current', ($current == $token->getId()) ? true : false);
|
||||
$session->setAttribute('current', ($current == $session->getId()) ? true : false);
|
||||
|
||||
$sessions[] = $token;
|
||||
$sessions[$key] = $session;
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
|
|
@ -1192,14 +1193,16 @@ App::delete('/v1/account/sessions/:sessionId')
|
|||
|
||||
$protocol = $request->getProtocol();
|
||||
$sessionId = ($sessionId === 'current')
|
||||
? Auth::tokenVerify($user->getAttribute('tokens'), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)
|
||||
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
|
||||
: $sessionId;
|
||||
|
||||
$tokens = $user->getAttribute('tokens', []);
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
|
||||
foreach ($tokens as $token) { /* @var $token Document */
|
||||
if (($sessionId == $token->getId()) && Auth::TOKEN_TYPE_LOGIN == $token->getAttribute('type')) {
|
||||
if (!$projectDB->deleteDocument($token->getId())) {
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Document $session */
|
||||
|
||||
if (($sessionId == $session->getId())) {
|
||||
if (!$projectDB->deleteDocument($session->getId())) {
|
||||
throw new Exception('Failed to remove token from DB', 500);
|
||||
}
|
||||
|
||||
|
|
@ -1215,10 +1218,10 @@ App::delete('/v1/account/sessions/:sessionId')
|
|||
;
|
||||
}
|
||||
|
||||
$token->setAttribute('current', false);
|
||||
$session->setAttribute('current', false);
|
||||
|
||||
if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
|
||||
$token->setAttribute('current', true);
|
||||
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
|
||||
$session->setAttribute('current', true);
|
||||
|
||||
$response
|
||||
->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
|
|
@ -1227,7 +1230,7 @@ App::delete('/v1/account/sessions/:sessionId')
|
|||
}
|
||||
|
||||
$events
|
||||
->setParam('payload', $response->output($token, Response::MODEL_SESSION))
|
||||
->setParam('payload', $response->output($session, Response::MODEL_SESSION))
|
||||
;
|
||||
|
||||
return $response->noContent();
|
||||
|
|
@ -1264,10 +1267,12 @@ App::delete('/v1/account/sessions')
|
|||
/** @var Appwrite\Event\Event $events */
|
||||
|
||||
$protocol = $request->getProtocol();
|
||||
$tokens = $user->getAttribute('tokens', []);
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
|
||||
foreach ($tokens as $token) { /* @var $token Document */
|
||||
if (!$projectDB->deleteDocument($token->getId())) {
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Document $session */
|
||||
|
||||
if (!$projectDB->deleteDocument($session->getId())) {
|
||||
throw new Exception('Failed to remove token from DB', 500);
|
||||
}
|
||||
|
||||
|
|
@ -1283,10 +1288,10 @@ App::delete('/v1/account/sessions')
|
|||
;
|
||||
}
|
||||
|
||||
$token->setAttribute('current', false);
|
||||
$session->setAttribute('current', false);
|
||||
|
||||
if ($token->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
|
||||
$token->setAttribute('current', true);
|
||||
if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too
|
||||
$session->setAttribute('current', true);
|
||||
$response
|
||||
->addCookie(Auth::$cookieName.'_legacy', '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
|
||||
->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
|
||||
|
|
@ -1296,8 +1301,8 @@ App::delete('/v1/account/sessions')
|
|||
|
||||
$events
|
||||
->setParam('payload', $response->output(new Document([
|
||||
'sum' => count($tokens),
|
||||
'sessions' => $tokens
|
||||
'sum' => count($sessions),
|
||||
'sessions' => $sessions
|
||||
]), Response::MODEL_SESSION_LIST))
|
||||
;
|
||||
|
||||
|
|
|
|||
|
|
@ -327,6 +327,7 @@ App::post('/v1/teams/:teamId/memberships')
|
|||
'registration' => \time(),
|
||||
'reset' => false,
|
||||
'name' => $name,
|
||||
'sessions' => [],
|
||||
'tokens' => [],
|
||||
], ['email' => $email]);
|
||||
} catch (Duplicate $th) {
|
||||
|
|
@ -595,10 +596,11 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status')
|
|||
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$secret = Auth::tokenGenerator();
|
||||
$session = new Document(array_merge([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'$collection' => Database::SYSTEM_COLLECTION_SESSIONS,
|
||||
'$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]],
|
||||
'userId' => $user->getId(),
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => $user->getAttribute('email'),
|
||||
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expiry,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
|
|
@ -606,7 +608,7 @@ App::patch('/v1/teams/:teamId/memberships/:inviteId/status')
|
|||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
|
||||
|
||||
$user->setAttribute('tokens', $session, Document::SET_TYPE_APPEND);
|
||||
$user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
|
||||
|
||||
Authorization::setRole('user:'.$userId);
|
||||
|
||||
|
|
|
|||
|
|
@ -196,21 +196,18 @@ App::get('/v1/users/:userId/sessions')
|
|||
throw new Exception('User not found', 404);
|
||||
}
|
||||
|
||||
$tokens = $user->getAttribute('tokens', []);
|
||||
$sessions = [];
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
$countries = $locale->getText('countries');
|
||||
|
||||
foreach ($tokens as $token) { /* @var $token Document */
|
||||
if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) {
|
||||
continue;
|
||||
}
|
||||
foreach ($sessions as $key => $session) {
|
||||
/** @var Document $session */
|
||||
|
||||
$token->setAttribute('countryName', (isset($countries[strtoupper($token->getAttribute('countryCode'))]))
|
||||
? $countries[strtoupper($token->getAttribute('countryCode'))]
|
||||
$session->setAttribute('countryName', (isset($countries[strtoupper($session->getAttribute('countryCode'))]))
|
||||
? $countries[strtoupper($session->getAttribute('countryCode'))]
|
||||
: $locale->getText('locale.country.unknown'));
|
||||
$token->setAttribute('current', false);
|
||||
$session->setAttribute('current', false);
|
||||
|
||||
$sessions[] = $token;
|
||||
$sessions[$key] = $session;
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
|
|
@ -434,11 +431,13 @@ App::delete('/v1/users/:userId/sessions/:sessionId')
|
|||
throw new Exception('User not found', 404);
|
||||
}
|
||||
|
||||
$tokens = $user->getAttribute('tokens', []);
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
|
||||
foreach ($tokens as $token) { /* @var $token Document */
|
||||
if ($sessionId == $token->getId()) {
|
||||
if (!$projectDB->deleteDocument($token->getId())) {
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Document $session */
|
||||
|
||||
if ($sessionId == $session->getId()) {
|
||||
if (!$projectDB->deleteDocument($session->getId())) {
|
||||
throw new Exception('Failed to remove token from DB', 500);
|
||||
}
|
||||
|
||||
|
|
@ -478,10 +477,12 @@ App::delete('/v1/users/:userId/sessions')
|
|||
throw new Exception('User not found', 404);
|
||||
}
|
||||
|
||||
$tokens = $user->getAttribute('tokens', []);
|
||||
$sessions = $user->getAttribute('sessions', []);
|
||||
|
||||
foreach ($tokens as $token) { /* @var $token Document */
|
||||
if (!$projectDB->deleteDocument($token->getId())) {
|
||||
foreach ($sessions as $session) {
|
||||
/** @var Document $session */
|
||||
|
||||
if (!$projectDB->deleteDocument($session->getId())) {
|
||||
throw new Exception('Failed to remove token from DB', 500);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@ App::setResource('user', function($mode, $project, $console, $request, $response
|
|||
|
||||
if (empty($user->getId()) // Check a document has been found in the DB
|
||||
|| Database::SYSTEM_COLLECTION_USERS !== $user->getCollection() // Validate returned document is really a user document
|
||||
|| !Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_LOGIN, Auth::$secret)) { // Validate user has valid login token
|
||||
|| !Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret)) { // Validate user has valid login token
|
||||
$user = new Document(['$id' => '', '$collection' => Database::SYSTEM_COLLECTION_USERS]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,13 +112,21 @@ class DeletesV1
|
|||
protected function deleteUser(Document $document, $projectId)
|
||||
{
|
||||
$tokens = $document->getAttribute('tokens', []);
|
||||
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
if (!$this->getProjectDB($projectId)->deleteDocument($token->getId())) {
|
||||
throw new Exception('Failed to remove token from DB');
|
||||
}
|
||||
}
|
||||
|
||||
$sessions = $document->getAttribute('sessions', []);
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
if (!$this->getProjectDB($projectId)->deleteDocument($session->getId())) {
|
||||
throw new Exception('Failed to remove session from DB');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Memberships
|
||||
$this->deleteByGroup([
|
||||
'$collection='.Database::SYSTEM_COLLECTION_MEMBERSHIPS,
|
||||
|
|
|
|||
|
|
@ -28,11 +28,17 @@ class Auth
|
|||
/**
|
||||
* Token Types.
|
||||
*/
|
||||
const TOKEN_TYPE_LOGIN = 1;
|
||||
const TOKEN_TYPE_LOGIN = 1; // Deprecated
|
||||
const TOKEN_TYPE_VERIFICATION = 2;
|
||||
const TOKEN_TYPE_RECOVERY = 3;
|
||||
const TOKEN_TYPE_INVITE = 4;
|
||||
|
||||
/**
|
||||
* Session Providers.
|
||||
*/
|
||||
const SESSION_PROVIDER_EMAIL = 'email';
|
||||
const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
|
||||
|
||||
/**
|
||||
* Token Expiration times.
|
||||
*/
|
||||
|
|
@ -207,6 +213,29 @@ class Auth
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify session and check that its not expired.
|
||||
*
|
||||
* @param array $sessions
|
||||
* @param string $secret
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function sessionVerify(array $sessions, string $secret)
|
||||
{
|
||||
foreach ($sessions as $session) { /** @var Document $session */
|
||||
if ($session->isSet('secret') &&
|
||||
$session->isSet('expire') &&
|
||||
$session->isSet('provider') &&
|
||||
$session->getAttribute('secret') === self::hash($secret) &&
|
||||
$session->getAttribute('expire') >= \time()) {
|
||||
return (string)$session->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is Previligged User?
|
||||
*
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class Database
|
|||
|
||||
// Auth, Account and Users (private to user)
|
||||
const SYSTEM_COLLECTION_USERS = 'users';
|
||||
const SYSTEM_COLLECTION_SESSIONS = 'sessions';
|
||||
const SYSTEM_COLLECTION_TOKENS = 'tokens';
|
||||
|
||||
// Teams (shared among team members)
|
||||
|
|
|
|||
70
src/Appwrite/Migration/Version/V07.php
Normal file
70
src/Appwrite/Migration/Version/V07.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Migration\Version;
|
||||
|
||||
use Appwrite\Migration\Migration;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\CLI\Console;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Appwrite\Database\Database;
|
||||
use Appwrite\Database\Document;
|
||||
|
||||
class V07 extends Migration
|
||||
{
|
||||
public function execute(): void
|
||||
{
|
||||
$db = $this->db;
|
||||
$project = $this->project;
|
||||
Console::log('Migrating project: ' . $project->getAttribute('name') . ' (' . $project->getId() . ')');
|
||||
|
||||
$this->forEachDocument([$this, 'fixDocument']);
|
||||
}
|
||||
|
||||
protected function fixDocument(Document $document)
|
||||
{
|
||||
$providers = Config::getParam('providers');
|
||||
|
||||
switch ($document->getAttribute('$collection')) {
|
||||
case Database::SYSTEM_COLLECTION_USERS:
|
||||
foreach ($providers as $key => $provider) {
|
||||
/**
|
||||
* Remove deprecated OAuth2 properties in the Users Documents.
|
||||
*/
|
||||
if (!empty($document->getAttribute('oauth2' . \ucfirst($key)))) {
|
||||
$document->removeAttribute('oauth2' . \ucfirst($key));
|
||||
}
|
||||
|
||||
if (!empty($document->getAttribute('oauth2' . \ucfirst($key) . 'AccessToken'))) {
|
||||
$document->removeAttribute('oauth2' . \ucfirst($key) . 'AccessToken');
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all Login Tokens, since they can't be migrated to the new structure.
|
||||
* Reason for it is the missing distinction between E-Mail and OAuth2 tokens.
|
||||
*/
|
||||
$tokens = array_filter($document->getAttribute('tokens', []), function($token) {
|
||||
return ($token->getAttribute('type') != Auth::TOKEN_TYPE_LOGIN);
|
||||
});
|
||||
|
||||
$document->setAttribute('tokens', array_values($tokens));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($document as &$attr) { // Handle child documents
|
||||
if ($attr instanceof Document) {
|
||||
$attr = $this->fixDocument($attr);
|
||||
}
|
||||
|
||||
if (\is_array($attr)) {
|
||||
foreach ($attr as &$child) {
|
||||
if ($child instanceof Document) {
|
||||
$child = $this->fixDocument($child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,24 @@ class Session extends Model
|
|||
'default' => 0,
|
||||
'example' => 1592981250,
|
||||
])
|
||||
->addRule('provider', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Session Provider.',
|
||||
'default' => '',
|
||||
'example' => 'email',
|
||||
])
|
||||
->addRule('providerUid', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Session Provider User ID.',
|
||||
'default' => '',
|
||||
'example' => 'user@example.com',
|
||||
])
|
||||
->addRule('providerToken', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'Session Provider Token.',
|
||||
'default' => '',
|
||||
'example' => 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3',
|
||||
])
|
||||
->addRule('ip', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'IP in use when the session was created.',
|
||||
|
|
|
|||
|
|
@ -62,41 +62,55 @@ class AuthTest extends TestCase
|
|||
$this->assertEquals(\mb_strlen(Auth::tokenGenerator(5)), 10);
|
||||
}
|
||||
|
||||
public function testTokenVerify()
|
||||
public function testSessionVerify()
|
||||
{
|
||||
$secret = 'secret1';
|
||||
$hash = Auth::hash($secret);
|
||||
$tokens1 = [
|
||||
new Document([
|
||||
'$id' => 'token1',
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'expire' => time() + 60 * 60 * 24,
|
||||
'secret' => $hash,
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
]),
|
||||
new Document([
|
||||
'$id' => 'token2',
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'expire' => time() - 60 * 60 * 24,
|
||||
'secret' => 'secret2',
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
]),
|
||||
];
|
||||
|
||||
$tokens2 = [
|
||||
new Document([ // Correct secret and type time, wrong expire time
|
||||
'$id' => 'token1',
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'expire' => time() - 60 * 60 * 24,
|
||||
'secret' => $hash,
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
]),
|
||||
new Document([
|
||||
'$id' => 'token2',
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'expire' => time() - 60 * 60 * 24,
|
||||
'secret' => 'secret2',
|
||||
'provider' => Auth::SESSION_PROVIDER_EMAIL,
|
||||
'providerUid' => 'test@example.com',
|
||||
]),
|
||||
];
|
||||
|
||||
$tokens3 = [ // Correct secret and expire time, wrong type
|
||||
$this->assertEquals(Auth::sessionVerify($tokens1, $secret), 'token1');
|
||||
$this->assertEquals(Auth::sessionVerify($tokens1, 'false-secret'), false);
|
||||
$this->assertEquals(Auth::sessionVerify($tokens2, $secret), false);
|
||||
$this->assertEquals(Auth::sessionVerify($tokens2, 'false-secret'), false);
|
||||
}
|
||||
|
||||
public function testTokenVerify()
|
||||
{
|
||||
$secret = 'secret1';
|
||||
$hash = Auth::hash($secret);
|
||||
$tokens1 = [
|
||||
new Document([
|
||||
'$id' => 'token1',
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
|
|
@ -105,20 +119,51 @@ class AuthTest extends TestCase
|
|||
]),
|
||||
new Document([
|
||||
'$id' => 'token2',
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'expire' => time() - 60 * 60 * 24,
|
||||
'secret' => 'secret2',
|
||||
]),
|
||||
];
|
||||
|
||||
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_LOGIN, $secret), 'token1');
|
||||
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_LOGIN, 'false-secret'), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_LOGIN, $secret), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_LOGIN, 'false-secret'), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_LOGIN, $secret), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_LOGIN, 'false-secret'), false);
|
||||
$tokens2 = [
|
||||
new Document([ // Correct secret and type time, wrong expire time
|
||||
'$id' => 'token1',
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'expire' => time() - 60 * 60 * 24,
|
||||
'secret' => $hash,
|
||||
]),
|
||||
new Document([
|
||||
'$id' => 'token2',
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'expire' => time() - 60 * 60 * 24,
|
||||
'secret' => 'secret2',
|
||||
]),
|
||||
];
|
||||
|
||||
$tokens3 = [ // Correct secret and expire time, wrong type
|
||||
new Document([
|
||||
'$id' => 'token1',
|
||||
'type' => Auth::TOKEN_TYPE_INVITE,
|
||||
'expire' => time() + 60 * 60 * 24,
|
||||
'secret' => $hash,
|
||||
]),
|
||||
new Document([
|
||||
'$id' => 'token2',
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'expire' => time() - 60 * 60 * 24,
|
||||
'secret' => 'secret2',
|
||||
]),
|
||||
];
|
||||
|
||||
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, $secret), 'token1');
|
||||
$this->assertEquals(Auth::tokenVerify($tokens1, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens2, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, $secret), false);
|
||||
$this->assertEquals(Auth::tokenVerify($tokens3, Auth::TOKEN_TYPE_RECOVERY, 'false-secret'), false);
|
||||
}
|
||||
|
||||
|
||||
public function testIsPreviliggedUser()
|
||||
{
|
||||
$this->assertEquals(false, Auth::isPreviliggedUser([]));
|
||||
|
|
|
|||
36
tests/unit/General/CollectionsTest.php
Normal file
36
tests/unit/General/CollectionsTest.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CollectionsTest extends TestCase
|
||||
{
|
||||
protected $collections;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->collections = require('app/config/collections.php');
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function testDuplicateRules()
|
||||
{
|
||||
foreach ($this->collections as $collection) {
|
||||
if ($collection['rules']) {
|
||||
foreach ($collection['rules'] as $check) {
|
||||
$occurences = 0;
|
||||
foreach ($collection['rules'] as $rule) {
|
||||
if ($rule['key'] == $check['key']) {
|
||||
$occurences++;
|
||||
}
|
||||
}
|
||||
$this->assertEquals(1, $occurences);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
tests/unit/Migration/MigrationV07Test.php
Normal file
69
tests/unit/Migration/MigrationV07Test.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Tests;
|
||||
|
||||
use ReflectionClass;
|
||||
use Appwrite\Migration\Version\V07;
|
||||
use Appwrite\Database\Database;
|
||||
use Appwrite\Database\Document;
|
||||
use Appwrite\Auth\Auth;
|
||||
use Utopia\Config\Config;
|
||||
|
||||
class MigrationV07Test extends MigrationTest
|
||||
{
|
||||
public function setUp(): void
|
||||
{
|
||||
Config::load('providers', __DIR__ . '/../../../app/config/providers.php');
|
||||
|
||||
$this->pdo = new \PDO('sqlite::memory:');
|
||||
$this->migration = new V07($this->pdo);
|
||||
$reflector = new ReflectionClass('Appwrite\Migration\Version\V07');
|
||||
$this->method = $reflector->getMethod('fixDocument');
|
||||
$this->method->setAccessible(true);
|
||||
}
|
||||
|
||||
public function testMigration()
|
||||
{
|
||||
$document = $this->fixDocument(new Document([
|
||||
'$id' => 'unique',
|
||||
'$collection' => Database::SYSTEM_COLLECTION_USERS,
|
||||
'oauth2Github' => 123,
|
||||
'oauth2GithubAccessToken' => 456,
|
||||
'tokens' => [
|
||||
new Document([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'userId' => 'unique',
|
||||
'type' => Auth::TOKEN_TYPE_LOGIN,
|
||||
'secret' => 'login',
|
||||
]),
|
||||
new Document([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'userId' => 'unique',
|
||||
'type' => Auth::TOKEN_TYPE_INVITE,
|
||||
'secret' => 'invite',
|
||||
]),
|
||||
new Document([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'userId' => 'unique',
|
||||
'type' => Auth::TOKEN_TYPE_RECOVERY,
|
||||
'secret' => 'recovery',
|
||||
]),
|
||||
new Document([
|
||||
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
|
||||
'userId' => 'unique',
|
||||
'type' => Auth::TOKEN_TYPE_VERIFICATION,
|
||||
'secret' => 'verification',
|
||||
]),
|
||||
]
|
||||
]));
|
||||
|
||||
$this->assertEquals($document->getAttribute('oauth2Github', null), null);
|
||||
$this->assertEquals($document->getAttribute('oauth2GithubAccessToken', null), null);
|
||||
|
||||
$this->assertCount(3, $document->getAttribute('tokens', []));
|
||||
$this->assertEquals(Auth::TOKEN_TYPE_INVITE, $document->getAttribute('tokens', [])[0]['type']);
|
||||
$this->assertEquals(Auth::TOKEN_TYPE_RECOVERY, $document->getAttribute('tokens', [])[1]['type']);
|
||||
$this->assertEquals(Auth::TOKEN_TYPE_VERIFICATION, $document->getAttribute('tokens', [])[2]['type']);
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue