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
+33 -2
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'));
}
}
+88
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);
}
}
+33
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;
}
}
+39
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}"
);
}
}
+42
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}",
]
);
}
}
+36
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}"
);
}
}
+61
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;
}
}
@@ -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',