1
0
Fork 0
mirror of synced 2024-06-09 06:14:43 +12:00

Merge pull request #1552 from appwrite/feat-magic-url

Feat Magic URL
This commit is contained in:
Eldad A. Fux 2021-09-02 10:09:55 +03:00 committed by GitHub
commit b23dae4948
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 536 additions and 16 deletions

View file

@ -10,6 +10,13 @@ return [
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateSession',
'enabled' => true,
],
'magic-url' => [
'name' => 'Magic URL',
'key' => 'usersAuthMagicURL',
'icon' => '/images/users/magic-url.png',
'docs' => 'https://appwrite.io/docs/client/account?sdk=web#accountCreateMagicURLSession',
'enabled' => true,
],
'anonymous' => [
'name' => 'Anonymous',
'key' => 'usersAuthAnonymous',

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": "Login",
"emails.magicSession.hello": "Hey,",
"emails.magicSession.body": "Follow this link to login.",
"emails.magicSession.footer": "If you didnt ask to login using 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,291 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
;
});
App::post('/v1/account/sessions/magic-url')
->desc('Create Magic URL session')
->groups(['api', 'account'])
->label('scope', 'public')
->label('auth.type', 'magic-url')
->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_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},email:{param-email}')
->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 */
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503);
}
$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();
$mails->setParam('event', 'users.create');
$audits->setParam('event', 'users.create');
}
$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 token 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);
}
if(empty($url)) {
$url = $request->getProtocol().'://'.$request->getHostname().'/auth/magic-url';
}
$url = Template::parseURL($url);
$url['query'] = Template::mergeQuery(((isset($url['query'])) ? $url['query'] : ''), ['userId' => $user->getId(), 'secret' => $loginSecret, 'expire' => $expire, 'project' => $project->getId()]);
$url = Template::unParseURL($url);
$mails
->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
->setAttribute('secret',
($isPrivilegedUser || $isAppUser) ? $loginSecret : '');
$audits
->setParam('userId', $user->getId())
->setParam('resource', 'users/'.$user->getId())
;
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN)
;
});
App::put('/v1/account/sessions/magic-url')
->desc('Create Magic URL session (confirmation)')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'account.sessions.create')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateMagicURLSession')
->label('sdk.description', '/docs/references/account/update-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 string $userId */
/** @var string $secret */
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Appwrite\Database\Database $projectDB */
/** @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,
],
]);
if (empty($profile)) {
throw new Exception('User not found', 404);
}
$token = Auth::tokenVerify($profile->getAttribute('tokens', []), Auth::TOKEN_TYPE_MAGIC_URL, $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('emailVerification', true);
$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);
}
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')
->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'])
@ -1461,6 +1746,10 @@ App::post('/v1/account/recovery')
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $events */
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503);
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
$isAppUser = Auth::isAppUser(Authorization::$roles);
@ -1551,7 +1840,7 @@ App::post('/v1/account/recovery')
});
App::put('/v1/account/recovery')
->desc('Complete Password Recovery')
->desc('Create Password Recovery (confirmation)')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'account.recovery.update')
@ -1664,6 +1953,10 @@ App::post('/v1/account/verification')
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $mails */
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503);
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
$isAppUser = Auth::isAppUser(Authorization::$roles);
@ -1738,7 +2031,7 @@ App::post('/v1/account/verification')
});
App::put('/v1/account/verification')
->desc('Complete Email Verification')
->desc('Create Email Verification (confirmation)')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'account.verification.update')

View file

@ -274,6 +274,10 @@ App::post('/v1/teams/:teamId/memberships')
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $mails */
if(empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception('SMTP Disabled', 503);
}
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::$roles);
$isAppUser = Auth::isAppUser(Authorization::$roles);

View file

@ -337,6 +337,7 @@ App::error(function ($error, $utopia, $request, $response, $layout, $project) {
case 412: // Error allowed publicly
case 429: // Error allowed publicly
case 501: // Error allowed publicly
case 503: // Error allowed publicly
$code = $error->getCode();
$message = $error->getMessage();
break;

View file

@ -141,6 +141,12 @@ App::init(function ($utopia, $request, $response, $project, $user) {
}
break;
case 'magic-url':
if($project->getAttribute('usersAuthMagicURL', true) === false) {
throw new Exception('Magic URL authentication is disabled for this project', 501);
}
break;
case 'anonymous':
if($project->getAttribute('usersAuthAnonymous', true) === false) {
throw new Exception('Anonymous authentication is disabled for this project', 501);

View file

@ -326,6 +326,7 @@ App::get('/console/users')
$page
->setParam('auth', Config::getParam('auth'))
->setParam('providers', Config::getParam('providers'))
->setParam('smtpEnabled', (!empty(App::getEnv('_APP_SMTP_HOST'))))
;
$layout

View file

@ -197,6 +197,24 @@ App::get('/auth/oauth2/success')
;
});
App::get('/auth/magic-url')
->groups(['web', 'home'])
->label('permission', 'public')
->label('scope', 'home')
->inject('layout')
->action(function ($layout) {
/** @var Utopia\View $layout */
$page = new View(__DIR__.'/../../views/home/auth/magicURL.phtml');
$layout
->setParam('title', APP_NAME)
->setParam('body', $page)
->setParam('header', [])
->setParam('footer', [])
;
});
App::get('/auth/oauth2/failure')
->groups(['web', 'home'])
->label('permission', 'public')

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

@ -1,6 +1,7 @@
<?php
$providers = $this->getParam('providers', []);
$auth = $this->getParam('auth', []);
$smtpEnabled = $this->getParam('smtpEnabled', false);
?>
<div class="cover">
@ -372,19 +373,26 @@ $auth = $this->getParam('auth', []);
data-failure-param-alert-text="Failed to update project auth status settings"
data-failure-param-alert-classname="error">
<input name="method" id="<?php echo $this->escape($key); ?>" type="hidden" autocomplete="off" value="<?php echo $this->escape($index); ?>">
<input name="status" type="hidden" data-forms-switch data-ls-bind="{{console-project.<?php echo $this->escape($key); ?>}}" data-cast-to="boolean" class="pull-end" />
<?php if( !(in_array($key, ['usersAuthMagicURL', 'usersAuthInvites']) && !$smtpEnabled)): ?>
<input name="status" type="hidden" data-forms-switch data-ls-bind="{{console-project.<?php echo $this->escape($key); ?>}}" data-cast-to="boolean" class="pull-end" />
<?php endif; ?>
</form>
<?php endif; ?>
<img src="<?php echo $this->escape($icon); ?>?buster=<?php echo APP_CACHE_BUSTER; ?>" alt="Email/Password Logo" class="pull-start provider margin-end" />
<span class="text-size-small"><?php echo $this->escape($name); ?><?php if(!$enabled): ?> <spann class="text-fade text-size-xs">soon</span><?php endif; ?></span>
<?php if($docs): ?>
<p class="margin-bottom-no text-one-liner text-size-small">
<a href="<?php echo $this->escape($docs); ?>" target="_blank" rel="noopener">Docs<i class="icon-link-ext"></i></a>
</p>
<span class="text-size-small"><?php echo $this->escape($name); ?><?php if(!$enabled): ?> <spann class="text-fade text-size-xs">soon</span><?php endif; ?>
<?php if( in_array($key, ['usersAuthMagicURL', 'usersAuthInvites']) && !$smtpEnabled): ?>
<p class="margin-bottom-no text-one-liner text-size-small text-danger">
SMTP Disabled
</p>
<?php else: ?>
<?php if($docs): ?>
<p class="margin-bottom-no text-one-liner text-size-small">
<a href="<?php echo $this->escape($docs); ?>" target="_blank" rel="noopener">Docs<i class="icon-link-ext"></i></a>
</p>
<?php endif; ?>
<?php endif; ?>
</div>
</li>

View file

@ -0,0 +1,38 @@
<div class="zone large padding margin-top" id="message" style="display: none">
<h1 class="margin-bottom">Missing Redirect URL</h1>
<p>Your Magic URL login flow is missing a proper redirect URL. Please check the
<a href="https://<?php echo APP_DOMAIN; ?>/docs/client/account?sdk=web#createMagicURLSession">Magic URL docs</a>
and send request for new session with a valid redirect URL.</p>
</div>
<script>
setTimeout(function () {
document.getElementById('message').style.display = 'block';
}, 25);
const urlSearchParams = new URLSearchParams(window.location.search);
const {
userId,
secret,
project
} = Object.fromEntries(urlSearchParams.entries());
const formData = new FormData();
formData.append('userId', userId);
formData.append('secret', secret);
const res = fetch(window.location.origin + '/v1/account/sessions/magic-url', {
method: 'PUT',
body: formData,
headers: {
["x-appwrite-project"]: project
}
}).then(response => response.json())
.then(data => {
if(data.$id) {
window.location = 'appwrite-callback-' + project + '://'+window.location.search;
}
})
</script>
<hr />

View file

@ -42,15 +42,14 @@ class MailsV1 extends Worker
$body = Template::fromFile(__DIR__ . '/../config/locale/templates/email-base.tpl');
$subject = '';
switch ($type) {
case MAIL_TYPE_RECOVERY:
$subject = $locale->getText("$prefix.subject");
break;
case MAIL_TYPE_INVITATION:
$subject = \sprintf($locale->getText("$prefix.subject"), $this->args['team'], $project);
$body->setParam('{{owner}}', $this->args['owner']);
$body->setParam('{{team}}', $this->args['team']);
break;
case MAIL_TYPE_RECOVERY:
case MAIL_TYPE_VERIFICATION:
case MAIL_TYPE_MAGIC_SESSION:
$subject = $locale->getText("$prefix.subject");
break;
default:
@ -127,6 +126,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

@ -0,0 +1 @@
Sends the user an email with a secret key for creating a session. When the user clicks the link in the email, the user is redirected back to the URL you provided with the secret key and userId values attached to the URL query string. Use the query string parameters to submit a request to the [PUT /account/sessions/magic-url](/docs/client/account#accountUpdateMagicURLSession) endpoint to complete the login process. The link sent to the user's email address is valid for 1 hour. If you are on a mobile device you can leave the URL parameter empty, so that the login completion will be handled by your Appwrite instance by default.

View file

@ -0,0 +1,3 @@
Use this endpoint to complete creating the session with the Magic URL. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST /account/sessions/magic-url](/docs/client/account#accountCreateMagicURLSession) endpoint.
Please note that in order to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -23,6 +23,10 @@ window.ls.router
template: "/auth/join?version=" + APP_ENV.CACHEBUSTER,
scope: "home"
})
.add("/auth/magic-url", {
template: "/auth/magic-url?version=" + APP_ENV.CACHEBUSTER,
scope: "home"
})
.add("/auth/oauth2/success", {
template: "/auth/oauth2/success?version=" + APP_ENV.CACHEBUSTER,
scope: "home"

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 = 'magic-url';
/**
* Token Expiration times.

View file

@ -1157,4 +1157,130 @@ trait AccountBase
return $data;
}
public function testCreateMagicUrl():array
{
$email = \time().'user@appwrite.io';
/**
* Test for SUCCESS
*/
$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,
// 'url' => 'http://localhost/magiclogin',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertEmpty($response['body']['secret']);
$this->assertIsNumeric($response['body']['expire']);
$userId = $response['body']['userId'];
$lastEmail = $this->getLastEmail();
$this->assertEquals($email, $lastEmail['to'][0]['address']);
$this->assertEquals('Login', $lastEmail['subject']);
$token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$expireTime = strpos($lastEmail['text'], 'expire='.$response['body']['expire'], 0);
$this->assertNotFalse($expireTime);
$secretTest = strpos($lastEmail['text'], 'secret='.$response['body']['secret'], 0);
$this->assertNotFalse($secretTest);
$userIDTest = strpos($lastEmail['text'], 'userId='.$response['body']['userId'], 0);
$this->assertNotFalse($userIDTest);
/**
* Test for FAILURE
*/
$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,
'url' => 'localhost/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,
'url' => 'http://remotehost/magiclogin',
]);
$this->assertEquals(400, $response['headers']['status-code']);
$data['token'] = $token;
$data['id'] = $userId;
return $data;
}
/**
* @depends testCreateMagicUrl
*/
public function testCreateSessionWithMagicUrl($data):array
{
$id = $data['id'] ?? '';
$token = $data['token'] ?? '';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/magic-url', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => $id,
'secret' => $token,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertIsArray($response['body']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['userId']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/magic-url', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'ewewe',
'secret' => $token,
]);
$this->assertEquals(404, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/magic-url', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => $id,
'secret' => 'sdasdasdasd',
]);
$this->assertEquals(401, $response['headers']['status-code']);
return $data;
}
}