1
0
Fork 0
mirror of synced 2024-05-21 05:02:37 +12:00

feat: initial phone authentication

This commit is contained in:
Torsten Dittmann 2022-06-08 11:00:38 +02:00
parent 94b2db96c2
commit 8ce669da6f
18 changed files with 833 additions and 9 deletions

4
.env
View file

@ -56,6 +56,10 @@ _APP_SMTP_PORT=1025
_APP_SMTP_SECURE=
_APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_PHONE_PROVIDER=mock
_APP_PHONE_USER=
_APP_PHONE_SECRET=
_APP_PHONE_FROM=
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_PREVIEW_LIMIT=20000000
_APP_FUNCTIONS_SIZE_LIMIT=30000000

View file

@ -190,6 +190,10 @@ ENV _APP_SERVER=swoole \
_APP_SMTP_SECURE= \
_APP_SMTP_USERNAME= \
_APP_SMTP_PASSWORD= \
_APP_PHONE_PROVIDER= \
_APP_PHONE_USER= \
_APP_PHONE_KEY= \
_APP_PHONE_FROM= \
_APP_FUNCTIONS_SIZE_LIMIT=30000000 \
_APP_FUNCTIONS_TIMEOUT=900 \
_APP_FUNCTIONS_CONTAINERS=10 \

View file

@ -981,6 +981,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'phone',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 320,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'status',
'type' => Database::VAR_BOOLEAN,
@ -1047,6 +1058,17 @@ $collections = [
'array' => false,
'filters' => [],
],
[
'$id' => 'phoneVerification',
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'reset',
'type' => Database::VAR_BOOLEAN,

View file

@ -389,6 +389,48 @@ return [
],
],
],
[
'category' => 'Phone',
'description' => '',
'variables' => [
[
'name' => '_APP_PHONE_PROVIDER',
'description' => '',
'introduction' => '',
'default' => '',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_PHONE_USER',
'description' => '',
'introduction' => '',
'default' => '',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_PHONE_SECRET',
'description' => '',
'introduction' => '',
'default' => '',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_PHONE_FROM',
'description' => '',
'introduction' => '',
'default' => '',
'required' => false,
'question' => '',
'filter' => ''
],
],
],
[
'category' => 'Storage',
'description' => '',

View file

@ -2,7 +2,9 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Phone;
use Appwrite\Auth\Validator\Password;
use Appwrite\Auth\Validator\Phone as ValidatorPhone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
@ -519,7 +521,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
], $detector->getOS(), $detector->getClient(), $detector->getDevice()));
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password'));
$isAnonymousUser = Auth::isAnonymousUser($user);
if ($isAnonymousUser) {
$user
@ -824,6 +826,228 @@ App::put('/v1/account/sessions/magic-url')
$response->dynamic($session, Response::MODEL_SESSION);
});
App::post('/v1/account/sessions/phone')
->desc('Create Phone session')
->groups(['api', 'account'])
->label('scope', 'public')
->label('auth.type', 'phone')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createPhoneSession')
->label('sdk.description', '/docs/references/account/create-phone-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('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('number', '', new ValidatorPhone, 'Phone number.')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('audits')
->inject('events')
->inject('phone')
->action(function (string $userId, string $number, Request $request, Response $response, Document $project, Database $dbForProject, Audit $audits, Event $events, Phone $phone) {
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
throw new Exception('Phone Disabled', 503, Exception::GENERAL_SMTP_DISABLED);
}
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$user = $dbForProject->findOne('users', [new Query('phone', Query::TYPE_EQUAL, [$number])]);
if (!$user) {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
if ($limit !== 0) {
$total = $dbForProject->count('users', max: APP_LIMIT_USERS);
if ($total >= $limit) {
throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED);
}
}
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
$user = Authorization::skip(fn () => $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'email' => null,
'phone' => $number,
'emailVerification' => false,
'phoneVerification' => false,
'status' => true,
'password' => null,
'passwordUpdate' => 0,
'registration' => \time(),
'reset' => false,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $number])
])));
}
$secret = $phone->generateSecretDigits();
$expire = \time() + Auth::TOKEN_EXPIRATION_PHONE;
$token = new Document([
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'type' => Auth::TOKEN_TYPE_PHONE,
'secret' => $secret,
'expire' => $expire,
'userAgent' => $request->getUserAgent('UNKNOWN'),
'ip' => $request->getIP(),
]);
Authorization::setRole('user:' . $user->getId());
$token = $dbForProject->createDocument('tokens', $token
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
$dbForProject->deleteCachedDocument('users', $user->getId());
$phone->send(APP::getEnv('_APP_PHONE_FROM'), $number, $secret);
$events->setPayload(
$response->output(
$token->setAttribute('secret', $secret),
Response::MODEL_TOKEN
)
);
// Hide secret for clients
$token->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $secret : '');
$audits
->setResource('user/' . $user->getId())
->setUser($user)
;
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($token, Response::MODEL_TOKEN)
;
});
App::put('/v1/account/sessions/phone')
->desc('Create Phone session (confirmation)')
->groups(['api', 'account'])
->label('scope', 'public')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePhoneSession')
->label('sdk.description', '/docs/references/account/update-phone-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 CustomId(), 'User ID.')
->param('secret', '', new Text(256), 'Valid verification token.')
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('locale')
->inject('geodb')
->inject('audits')
->inject('events')
->action(function (string $userId, string $secret, Request $request, Response $response, Database $dbForProject, Locale $locale, Reader $geodb, Audit $audits, Event $events) {
$user = Authorization::skip(fn() => $dbForProject->getDocument('users', $userId));
if ($user->isEmpty()) {
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$token = Auth::phoneTokenVerify($user->getAttribute('tokens', []), $secret);
if (!$token) {
throw new Exception('Invalid login token', 401, Exception::USER_INVALID_TOKEN);
}
$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(
[
'$id' => $dbForProject->getId(),
'userId' => $user->getId(),
'provider' => Auth::SESSION_PROVIDER_PHONE,
'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:' . $user->getId());
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $user->getId()])
->setAttribute('$write', ['user:' . $user->getId()]));
$dbForProject->deleteCachedDocument('users', $user->getId());
/**
* We act like we're updating and validating
* the recovery token but actually we don't need it anymore.
*/
$dbForProject->deleteDocument('tokens', $token);
$dbForProject->deleteCachedDocument('users', $user->getId());
$user->setAttribute('phoneVerification', true);
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
if (false === $user) {
throw new Exception('Failed saving user to DB', 500, Exception::GENERAL_SERVER_ERROR);
}
$audits->setResource('user/' . $user->getId());
$events
->setParam('userId', $user->getId())
->setParam('sessionId', $session->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 = $locale->getText('countries.' . strtolower($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'])
@ -1285,7 +1509,7 @@ App::patch('/v1/account/email')
->inject('events')
->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
if (
!$isAnonymousUser &&

View file

@ -551,11 +551,6 @@ App::patch('/v1/users/:userId/email')
throw new Exception('User not found', 404, Exception::USER_NOT_FOUND);
}
$isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting
if (!$isAnonymousUser) {
//TODO: Remove previous unique ID.
}
$email = \strtolower($email);
$user

View file

@ -23,6 +23,10 @@ use Ahc\Jwt\JWT;
use Ahc\Jwt\JWTException;
use Appwrite\Extend\Exception;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Phone\Mock;
use Appwrite\Auth\Phone\Telesign;
use Appwrite\Auth\Phone\TextMagic;
use Appwrite\Auth\Phone\Twilio;
use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
@ -64,6 +68,8 @@ use Utopia\Storage\Device\S3;
use Utopia\Storage\Device\Linode;
use Utopia\Storage\Device\Wasabi;
use function PHPUnit\Framework\matches;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address
@ -960,3 +966,17 @@ App::setResource('geodb', function ($register) {
/** @var Utopia\Registry\Registry $register */
return $register->get('geodb');
}, ['register']);
App::setResource('phone', function () {
$provider = App::getEnv('_APP_PHONE_PROVIDER');
$user = App::getEnv('_APP_PHONE_USER');
$secret = App::getEnv('_APP_PHONE_SECRET');
return match ($provider) {
'mock' => new Mock('', ''), // used for tests
'twilio' => new Twilio($user, $secret),
'text-magic' => new TextMagic($user, $secret),
'telesign' => new Telesign($user, $secret),
default => null
};
});

View file

@ -175,6 +175,10 @@ services:
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_PHONE_PROVIDER
- _APP_PHONE_FROM
- _APP_PHONE_USER
- _APP_PHONE_SECRET
appwrite-realtime:
entrypoint: realtime

View file

@ -27,6 +27,7 @@ class Auth
public const TOKEN_TYPE_RECOVERY = 3;
public const TOKEN_TYPE_INVITE = 4;
public const TOKEN_TYPE_MAGIC_URL = 5;
public const TOKEN_TYPE_PHONE = 6;
/**
* Session Providers.
@ -34,6 +35,7 @@ class Auth
public const SESSION_PROVIDER_EMAIL = 'email';
public const SESSION_PROVIDER_ANONYMOUS = 'anonymous';
public const SESSION_PROVIDER_MAGIC_URL = 'magic-url';
public const SESSION_PROVIDER_PHONE = 'phone';
/**
* Token Expiration times.
@ -42,6 +44,7 @@ class Auth
public const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 24 * 7; /* 7 days */
public const TOKEN_EXPIRATION_PHONE = 60 * 15; /* 15 minutes */
/**
* @var string
@ -195,7 +198,8 @@ class Auth
*/
public static function tokenVerify(array $tokens, int $type, string $secret)
{
foreach ($tokens as $token) { /** @var Document $token */
foreach ($tokens as $token) {
/** @var Document $token */
if (
$token->isSet('type') &&
$token->isSet('secret') &&
@ -211,6 +215,25 @@ class Auth
return false;
}
public static function phoneTokenVerify(array $tokens, string $secret)
{
foreach ($tokens as $token) {
/** @var Document $token */
if (
$token->isSet('type') &&
$token->isSet('secret') &&
$token->isSet('expire') &&
$token->getAttribute('type') == Auth::TOKEN_TYPE_PHONE &&
$token->getAttribute('secret') === $secret &&
$token->getAttribute('expire') >= \time()
) {
return (string) $token->getId();
}
}
return false;
}
/**
* Verify session and check that its not expired.
*
@ -221,7 +244,8 @@ class Auth
*/
public static function sessionVerify(array $sessions, string $secret)
{
foreach ($sessions as $session) { /** @var Document $session */
foreach ($sessions as $session) {
/** @var Document $session */
if (
$session->isSet('secret') &&
$session->isSet('expire') &&
@ -303,4 +327,11 @@ class Auth
return $roles;
}
public static function isAnonymousUser(Document $user): bool
{
return (is_null($user->getAttribute('email'))
|| is_null($user->getAttribute('phone'))
) && is_null($user->getAttribute('password'));
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Appwrite\Auth;
use Appwrite\Extend\Exception;
abstract class Phone
{
/**
* @var string
*/
protected string $user;
/**
* @var string
*/
protected string $secret;
/**
* @param string $key
*/
public function __construct(string $user, string $secret)
{
$this->user = $user;
$this->secret = $secret;
}
/**
* Send Message to phone.
* @param string $from
* @param string $to
* @param string $message
* @return void
*/
abstract public function send(string $from, string $to, string $message): void;
/**
* @param string $method
* @param string $url
* @param array $headers
* @param string $payload
*
* @return string
*/
protected function request(string $method, string $url, array $headers = [], ?string $payload = null, ?string $userpwd = null): string
{
$ch = \curl_init($url);
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
\curl_setopt($ch, CURLOPT_HEADER, 0);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
\curl_setopt($ch, CURLOPT_USERAGENT, 'Appwrite Phone Authentication');
if (!is_null($payload)) {
\curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
}
if (!is_null($userpwd)) {
\curl_setopt($ch, CURLOPT_USERPWD, $userpwd);
}
$headers[] = 'Content-length: ' . \strlen($payload);
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = (string) \curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
\curl_close($ch);
if ($code >= 400) {
throw new Exception($response);
}
return $response;
}
/**
* Generate 6 random digits for phone verification.
*
* @param int $digits
* @return string
*/
public function generateSecretDigits(int $digits = 6): string
{
return substr(str_shuffle("0123456789"), 0, $digits);
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Appwrite\Auth\Phone;
use Appwrite\Auth\Phone;
class Mock extends Phone
{
/**
* @var string
*/
static public string $defaultDigits = '123456';
/**
* @param string $from
* @param string $to
* @param string $message
* @return void
*/
public function send(string $from, string $to, string $message): void
{
return;
}
/**
* @param int $digits
* @return string
*/
public function generateSecretDigits(int $digits = 6): string
{
return self::$defaultDigits;
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Appwrite\Auth\Phone;
use Appwrite\Auth\Phone;
// Reference Material
// https://www.twilio.com/docs/sms/api
class Telesign extends Phone
{
/**
* @var string
*/
private string $endpoint = 'https://rest-api.telesign.com/v1/messaging';
/**
* @param string $from
* @param string $to
* @param string $message
* @return void
* @throws \Appwrite\Extend\Exception
*/
public function send(string $from, string $to, string $message): void
{
$to = ltrim($to, '+');
$this->request(
method: 'POST',
url: $this->endpoint,
payload: \http_build_query([
'message' => $message,
'message_type' => 'otp',
'phone_number' => $to
]),
userpwd: "{$this->user}:{$this->secret}"
);
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Appwrite\Auth\Phone;
use Appwrite\Auth\Phone;
// Reference Material
// https://www.textmagic.com/docs/api/start/
class TextMagic extends Phone
{
/**
* @var string
*/
private string $endpoint = 'https://rest.textmagic.com/api/v2';
/**
* @param string $from
* @param string $to
* @param string $message
* @return void
*/
public function send(string $from, string $to, string $message): void
{
$to = ltrim($to, '+');
$from = ltrim($from, '+');
$this->request(
method: 'POST',
url: $this->endpoint . '/messages',
payload: \http_build_query([
'text' => $message,
'from' => $from,
'phones' => $to
]),
headers: [
"X-TM-Username: {$this->user}",
"X-TM-Key: {$this->secret}",
]
);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Appwrite\Auth\Phone;
use Appwrite\Auth\Phone;
// Reference Material
// https://www.twilio.com/docs/sms/api
class Twilio extends Phone
{
/**
* @var string
*/
private string $endpoint = 'https://api.twilio.com/2010-04-01';
/**
* @param string $from
* @param string $to
* @param string $message
* @return void
*/
public function send(string $from, string $to, string $message): void
{
$this->request(
method: 'POST',
url: "{$this->endpoint}/Accounts/{$this->user}/Messages.json",
payload: \http_build_query([
'Body' => $message,
'From' => $from,
'To' => $to
]),
userpwd: "{$this->user}:{$this->secret}"
);
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Appwrite\Auth\Validator;
use Utopia\Validator;
/**
* Phone.
*
* Validates a number for the E.164 format.
*/
class Phone extends Validator
{
/**
* Get Description.
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return "Phone number must start with a '+' can have a maximum of fifteen digits.";
}
/**
* Is valid.
*
* @param mixed $value
*
* @return bool
*/
public function isValid($value): bool
{
return !!\preg_match('/^\+[1-9]\d{1,14}$/', $value);
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}

View file

@ -47,12 +47,24 @@ class User extends Model
'default' => '',
'example' => 'john@appwrite.io',
])
->addRule('phone', [
'type' => self::TYPE_STRING,
'description' => 'User phone number.',
'default' => '',
'example' => '+49 30 901820',
])
->addRule('emailVerification', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Email verification status.',
'default' => false,
'example' => true,
])
->addRule('phoneVerification', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Phone verification status.',
'default' => false,
'example' => true,
])
->addRule('prefs', [
'type' => Response::MODEL_PREFERENCES,
'description' => 'User preferences as a key-value object',

View file

@ -2,6 +2,7 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Auth\Phone\Mock;
use Tests\E2E\Client;
trait AccountBase
@ -1329,6 +1330,132 @@ trait AccountBase
return $data;
}
public function testCreatePhone(): array
{
$number = '+1 234 56789';
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/phone', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'number' => $number,
]);
$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'];
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/phone', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()'
]);
$this->assertEquals(400, $response['headers']['status-code']);
$data['token'] = Mock::$defaultDigits;
$data['id'] = $userId;
$data['number'] = $number;
return $data;
}
/**
* @depends testCreatePhone
*/
public function testCreateSessionWithPhone($data): void
{
$id = $data['id'] ?? '';
$token = $data['token'] ?? '';
$number = $data['number'] ?? '';
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', 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/phone', 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']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', 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']);
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertIsNumeric($response['body']['registration']);
$this->assertEquals($response['body']['phone'], $number);
$this->assertTrue($response['body']['phoneVerification']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_PUT, '/account/sessions/phone', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => $id,
'secret' => $token,
]);
$this->assertEquals(401, $response['headers']['status-code']);
}
/**
* @depends testCreateMagicUrl
*/

View file

@ -0,0 +1,40 @@
<?php
namespace Appwrite\Tests;
use Appwrite\Auth\Validator\Phone;
use PHPUnit\Framework\TestCase;
class PhoneTest extends TestCase
{
protected ?Phone $object = null;
public function setUp(): void
{
$this->object = new Phone();
}
public function tearDown(): void
{
}
public function testValues()
{
$this->assertEquals($this->object->isValid(false), false);
$this->assertEquals($this->object->isValid(null), false);
$this->assertEquals($this->object->isValid(''), false);
$this->assertEquals($this->object->isValid('+1'), false);
$this->assertEquals($this->object->isValid('8989829304'), false);
$this->assertEquals($this->object->isValid('786-307-3615'), false);
$this->assertEquals($this->object->isValid('+16308A520397'), false);
$this->assertEquals($this->object->isValid('+0415553452342'), false);
$this->assertEquals($this->object->isValid('+14155552'), true);
$this->assertEquals($this->object->isValid('+141555526'), true);
$this->assertEquals($this->object->isValid('+16308520394'), true);
$this->assertEquals($this->object->isValid('+163085205339'), true);
$this->assertEquals($this->object->isValid('+5511552563253'), true);
$this->assertEquals($this->object->isValid('+55115525632534'), true);
$this->assertEquals($this->object->isValid('+919367788755111'), true);
}
}