1
0
Fork 0
mirror of synced 2024-06-01 10:29:48 +12:00

create session with magic url

This commit is contained in:
Damodar Lohani 2021-08-30 16:29:52 +05:45
parent 09aea0fd95
commit 22ceb1dc7c
5 changed files with 281 additions and 0 deletions

View file

@ -9,6 +9,12 @@
"emails.verification.footer": "If you didnt ask to verify this address, you can ignore this message.",
"emails.verification.thanks": "Thanks",
"emails.verification.signature": "{{project}} team",
"emails.magicSession.subject": "Create Session",
"emails.magicSession.hello": "Hey,",
"emails.magicSession.body": "Follow this link to create a session.",
"emails.magicSession.footer": "If you didnt ask to create session for this email, you can ignore this message.",
"emails.magicSession.thanks": "Thanks",
"emails.magicSession.signature": "{{project}} team",
"emails.recovery.subject": "Password Reset",
"emails.recovery.hello": "Hello {{name}}",
"emails.recovery.body": "Follow this link to reset your {{project}} password.",

View file

@ -611,6 +611,273 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
;
});
App::post('/v1/account/sessions/url')
->desc('Create Magic URL for creating sessions')
->groups(['api', 'account'])
->label('scope', 'account')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createMagicURL')
->label('sdk.description', '/docs/references/account/create-magic-url.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TOKEN)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{userId}')
->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('locale')
->inject('audits')
->inject('events')
->inject('mails')
->action(function ($email, $url, $request, $response, $project, $projectDB, $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\Locale\Locale $locale */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $mails */
$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,
],
]);
if (empty($user)) {
$limit = $project->getAttribute('usersAuthLimit', 0);
if ($limit !== 0) {
$projectDB->getCollection([ // Count users
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_USERS,
],
]);
$sum = $projectDB->getSum();
if($sum >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501);
}
}
Authorization::disable();
$user = $projectDB->createDocument([
'$collection' => Database::SYSTEM_COLLECTION_USERS,
'$permissions' => [
'read' => ['*'],
'write' => ['user:{self}'],
],
'email' => $email,
'emailVerification' => false,
'status' => Auth::USER_STATUS_UNACTIVATED,
'password' => null,
'passwordUpdate' => \time(),
'registration' => \time(),
'reset' => false,
'name' => null,
], ['email' => $email]);
Authorization::reset();
$userAdded = true;
}
$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()]],
'userId' => $user->getId(),
'type' => Auth::TOKEN_TYPE_MAGIC_URL,
'secret' => Auth::hash($loginSecret), // One way hash encryption to protect DB leak
'expire' => $expire,
'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 verification to DB', 500);
}
$user->setAttribute('tokens', $token, Document::SET_TYPE_APPEND);
$user = $projectDB->updateDocument($user->getArrayCopy());
if (false === $user) {
throw new Exception('Failed to save user to DB', 500);
}
$url = Template::parseURL($url);
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $loginSecret, 'expire' => $expire]);
$url = Template::unParseURL($url);
$mails
->setParam('event', ($userAdded ?? false) ? 'users.create' : '')
->setParam('from', $project->getId())
->setParam('recipient', $user->getAttribute('email'))
->setParam('url', $url)
->setParam('locale', $locale->default)
->setParam('project', $project->getAttribute('name', ['[APP-NAME]']))
->setParam('type', MAIL_TYPE_MAGIC_SESSION)
->trigger()
;
$events
->setParam('eventData',
$response->output($token->setAttribute('secret', $loginSecret),
Response::MODEL_TOKEN
))
;
$token // Hide secret for clients, sp
->setAttribute('secret',
($isPrivilegedUser || $isAppUser) ? $loginSecret : '');
$audits
->setParam('userId', $user->getId())
->setParam('event', ($userAdded ?? false) ? 'users.create' : '')
->setParam('resource', 'users/'.$user->getId())
;
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN)
;
});
App::put('/v1/account/sessions/url')
->desc('Create session with Magic URL')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'account.sessions.create')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createMagicUrlSession')
->label('sdk.description', '/docs/references/account/create-magic-url-session.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->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('secret', '', new Text(256), 'Valid verification token.')
->inject('request')
->inject('response')
->inject('projectDB')
->inject('locale')
->inject('geodb')
->inject('audits')
->action(function ($userId, $secret, $request, $response, $projectDB, $locale, $geodb, $audits) {
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Database $projectDB */
/** @var Appwrite\Event\Event $audits */
$profile = $projectDB->getCollectionFirst([ // Get user by email address
'limit' => 1,
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_USERS,
'$id='.$userId,
],
]);
if (empty($profile)) {
throw new Exception('User not found', 404);
}
$token = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_VERIFICATION, $secret);
if (!$token) {
throw new Exception('Invalid login token', 401);
}
$detector = new Detector($request->getUserAgent('UNKNOWN'));
$record = $geodb->get($request->getIP());
$secret = Auth::tokenGenerator();
$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(),
'provider' => Auth::SESSION_PROVIDER_MAGIC_URL,
'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
],
$detector->getOS(),
$detector->getClient(),
$detector->getDevice()
));
Authorization::setRole('user:'.$profile->getId());
$session = $projectDB->createDocument($session->getArrayCopy());
if (false === $session) {
throw new Exception('Failed saving session to DB', 500);
}
$profile->setAttribute('sessions', $session, Document::SET_TYPE_APPEND);
$user = $projectDB->updateDocument($profile->getArrayCopy());
if (false === $user) {
throw new Exception('Failed saving user to DB', 500);
}
$audits
->setParam('userId', $user->getId())
->setParam('event', 'account.sessions.create')
->setParam('resource', 'users/'.$user->getId())
;
if (!Config::getParam('domainVerification')) {
$response
->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)]))
;
}
$protocol = $request->getProtocol();
$response
->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null)
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite'))
->setStatusCode(Response::STATUS_CODE_CREATED)
;
$countryName = (isset($countries[strtoupper($session->getAttribute('countryCode'))]))
? $countries[strtoupper($session->getAttribute('countryCode'))]
: $locale->getText('locale.country.unknown');
$session
->setAttribute('current', true)
->setAttribute('countryName', $countryName)
;
$response->dynamic($session, Response::MODEL_SESSION);
});
App::post('/v1/account/sessions/anonymous')
->desc('Create Anonymous Session')
->groups(['api', 'account', 'auth'])

View file

@ -72,6 +72,7 @@ const DELETE_TYPE_ABUSE = 'abuse';
const DELETE_TYPE_CERTIFICATES = 'certificates';
// Mail Types
const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
const MAIL_TYPE_RECOVERY = 'recovery';
const MAIL_TYPE_INVITATION = 'invitation';
// Auth Types

View file

@ -58,6 +58,9 @@ class MailsV1 extends Worker
case MAIL_TYPE_VERIFICATION:
$subject = $locale->getText("$prefix.subject");
break;
case MAIL_TYPE_MAGIC_SESSION:
$subject = $locale->getText("$prefix.subject");
break;
default:
throw new Exception('Undefined Mail Type : ' . $type, 500);
}
@ -132,6 +135,8 @@ class MailsV1 extends Worker
return 'emails.invitation';
case MAIL_TYPE_VERIFICATION:
return 'emails.verification';
case MAIL_TYPE_MAGIC_SESSION:
return 'emails.magicSession';
default:
throw new Exception('Undefined Mail Type : ' . $type, 500);
}

View file

@ -32,12 +32,14 @@ class Auth
const TOKEN_TYPE_VERIFICATION = 2;
const TOKEN_TYPE_RECOVERY = 3;
const TOKEN_TYPE_INVITE = 4;
const TOKEN_TYPE_MAGIC_URL = 5;
/**
* Session Providers.
*/
const SESSION_PROVIDER_EMAIL = 'email';
const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
const SESSION_PROVIDER_MAGIC_URL = 'magicUrl';
/**
* Token Expiration times.