1
0
Fork 0
mirror of synced 2024-06-02 19:04:49 +12:00

fix magic url

This commit is contained in:
Torsten Dittmann 2021-10-07 21:10:43 +02:00
parent 1dc8a74e57
commit e368053d7b
2 changed files with 82 additions and 79 deletions

View file

@ -620,21 +620,22 @@ App::post('/v1/account/sessions/magic-url')
->label('sdk.response.model', Response::MODEL_TOKEN)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},email:{param-email}')
->param('userId', '', new CustomId(), 'Unique Id. Choose your own unique ID or pass the string `unique()` to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('email', '', new Email(), 'User email.')
->param('url', '', function ($clients) { return new Host($clients); }, 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->inject('request')
->inject('response')
->inject('project')
->inject('projectDB')
->inject('dbForInternal')
->inject('locale')
->inject('audits')
->inject('events')
->inject('mails')
->action(function ($email, $url, $request, $response, $project, $projectDB, $locale, $audits, $events, $mails) {
->action(function ($userId, $email, $url, $request, $response, $project, $dbForInternal, $locale, $audits, $events, $mails) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Document $project */
/** @var Appwrite\Database\Database $projectDB */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Locale\Locale $locale */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
@ -647,50 +648,44 @@ App::post('/v1/account/sessions/magic-url')
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
$isAppUser = Auth::isAppUser(Authorization::$roles);
$user = $projectDB->getCollectionFirst([ // Get user by email address
'limit' => 1,
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_USERS,
'email='.$email,
],
]);
$user = $dbForInternal->findOne('users', [new Query('email', Query::TYPE_EQUAL, [$email])]);
if (empty($user)) {
$limit = $project->getAttribute('usersAuthLimit', 0);
if (!$user) {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
$projectDB->getCollection([ // Count users
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_USERS,
],
]);
$sum = $dbForInternal->count('users', [
new Query('deleted', Query::TYPE_EQUAL, [false]),
], APP_LIMIT_COUNT);
$sum = $projectDB->getSum();
if($sum >= $limit) {
if ($sum >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501);
}
}
Authorization::disable();
$userId = $userId == 'unique()' ? $dbForInternal->getId() : $userId;
$user = $projectDB->createDocument([
'$collection' => Database::SYSTEM_COLLECTION_USERS,
'$permissions' => [
'read' => ['*'],
'write' => ['user:{self}'],
],
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => null,
'passwordUpdate' => \time(),
'registration' => \time(),
'reset' => false,
'name' => null,
], ['email' => $email]);
$user = Authorization::skip(function () use ($dbForInternal, $userId, $email) {
return $dbForInternal->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => null,
'passwordUpdate' => \time(),
'registration' => \time(),
'reset' => false,
'prefs' => [],
'sessions' => [],
'tokens' => [],
'memberships' => [],
'search' => implode(' ', [$userId, $email]),
'deleted' => false
]));
});
Authorization::reset();
$mails->setParam('event', 'users.create');
$audits->setParam('event', 'users.create');
}
@ -698,10 +693,9 @@ App::post('/v1/account/sessions/magic-url')
$loginSecret = Auth::tokenGenerator();
$expire = \time() + Auth::TOKEN_EXPIRATION_CONFIRM;
$token = new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'$permissions' => ['read' => ['user:'.$user->getId()], 'write' => ['user:'.$user->getId()]],
'$id' => $dbForInternal->getId(),
'userId' => $user->getId(),
'type' => Auth::TOKEN_TYPE_MAGIC_URL,
'secret' => Auth::hash($loginSecret), // One way hash encryption to protect DB leak
@ -709,18 +703,12 @@ App::post('/v1/account/sessions/magic-url')
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
]);
Authorization::setRole('user:'.$user->getId());
$token = $projectDB->createDocument($token->getArrayCopy());
if (false === $token) {
throw new Exception('Failed saving token to DB', 500);
}
$user->setAttribute('tokens', $token, Document::SET_TYPE_APPEND);
$user = $projectDB->updateDocument($user->getArrayCopy());
$user = $dbForInternal->updateDocument('users', $user->getId(), $user);
if (false === $user) {
throw new Exception('Failed to save user to DB', 500);
@ -780,37 +768,31 @@ App::put('/v1/account/sessions/magic-url')
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{param-userId}')
->param('userId', '', new UID(), 'User unique ID.')
->param('userId', '', new CustomId(), 'User unique ID.')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('request')
->inject('response')
->inject('projectDB')
->inject('dbForInternal')
->inject('locale')
->inject('geodb')
->inject('audits')
->action(function ($userId, $secret, $request, $response, $projectDB, $locale, $geodb, $audits) {
->action(function ($userId, $secret, $request, $response, $dbForInternal, $locale, $geodb, $audits) {
/** @var string $userId */
/** @var string $secret */
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Database $projectDB */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Utopia\Locale\Locale $locale */
/** @var MaxMind\Db\Reader $geodb */
/** @var Appwrite\Event\Event $audits */
$profile = $projectDB->getCollectionFirst([ // Get user by user ID
'limit' => 1,
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_USERS,
'$id='.$userId,
],
]);
$user = $dbForInternal->getDocument('users', $userId);
if (empty($profile)) {
if ($user->isEmpty() || $user->getAttribute('deleted')) {
throw new Exception('User not found', 404);
}
$token = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_MAGIC_URL, $secret);
$token = Auth::tokenVerify($user->getAttribute('tokens', []), Auth::TOKEN_TYPE_MAGIC_URL, $secret);
if (!$token) {
throw new Exception('Invalid login token', 401);
@ -822,9 +804,8 @@ App::put('/v1/account/sessions/magic-url')
$expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$session = new Document(array_merge(
[
'$collection' => Database::SYSTEM_COLLECTION_SESSIONS,
'$permissions' => ['read' => ['user:' . $profile->getId()], 'write' => ['user:' . $profile->getId()]],
'userId' => $profile->getId(),
'$id' => $dbForInternal->getId(),
'userId' => $user->getId(),
'provider' => Auth::SESSION_PROVIDER_MAGIC_URL,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
@ -837,27 +818,36 @@ App::put('/v1/account/sessions/magic-url')
$detector->getDevice()
));
Authorization::setRole('user:'.$profile->getId());
Authorization::setRole('user:' . $user->getId());
$session = $projectDB->createDocument($session->getArrayCopy());
$session = $dbForInternal->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()])
);
if (false === $session) {
throw new Exception('Failed saving session to DB', 500);
$tokens = $user->getAttribute('tokens', []);
/**
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
foreach ($tokens as $key => $singleToken) {
if ($token === $singleToken->getId()) {
unset($tokens[$key]);
}
}
$profile->setAttribute('emailVerification', true);
$profile->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
$user
->setAttribute('sessions', $session, Document::SET_TYPE_APPEND)
->setAttribute('tokens', $tokens);
$user = $projectDB->updateDocument($profile->getArrayCopy());
$user = $dbForInternal->updateDocument('users', $user->getId(), $user);
if (false === $user) {
throw new Exception('Failed saving user to DB', 500);
}
if (!$projectDB->deleteDocument($token)) {
throw new Exception('Failed to remove login token from DB', 500);
}
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.create')

View file

@ -1176,6 +1176,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'email' => $email,
// 'url' => 'http://localhost/magiclogin',
]);
@ -1213,6 +1214,7 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'email' => $email,
'url' => 'localhost/magiclogin',
]);
@ -1224,12 +1226,23 @@ trait AccountBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'email' => $email,
'url' => 'http://remotehost/magiclogin',
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => $email,
]);
$this->assertEquals(400, $response['headers']['status-code']);
$data['token'] = $token;
$data['id'] = $userId;
@ -1243,7 +1256,7 @@ trait AccountBase
{
$id = $data['id'] ?? '';
$token = $data['token'] ?? '';
/**
* Test for SUCCESS
*/
@ -1261,7 +1274,7 @@ trait AccountBase
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['userId']);
/**
* Test for FAILURE
*/
@ -1286,7 +1299,7 @@ trait AccountBase
]);
$this->assertEquals(401, $response['headers']['status-code']);
return $data;
}
}