mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
feat: initial phone authentication
This commit is contained in:
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user