Merge pull request #2747 from appwrite/feat-password-hash-algos

Password Hashing algos
This commit is contained in:
Eldad A. Fux 2022-08-16 22:30:23 +03:00 committed by GitHub
commit ccb8a1144c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 4201 additions and 1311 deletions

4
.env
View file

@ -56,8 +56,8 @@ _APP_SMTP_PORT=1025
_APP_SMTP_SECURE=
_APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_PHONE_PROVIDER=phone://mock
_APP_PHONE_FROM=+123456789
_APP_SMS_PROVIDER=sms://mock
_APP_SMS_FROM=+123456789
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_PREVIEW_LIMIT=20000000
_APP_FUNCTIONS_SIZE_LIMIT=30000000

View file

@ -123,6 +123,33 @@ RUN \
./configure && \
make && make install
# Rust Extensions Compile Image
FROM php:8.0.18-cli as rust_compile
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH=/root/.cargo/bin:$PATH
RUN apt-get update && apt-get install musl-tools build-essential clang-11 git -y
RUN rustup target add $(uname -m)-unknown-linux-musl
# Install ZigBuild for easier cross-compilation
RUN curl https://ziglang.org/builds/zig-linux-$(uname -m)-0.10.0-dev.2674+d980c6a38.tar.xz --output /tmp/zig.tar.xz
RUN tar -xf /tmp/zig.tar.xz -C /tmp/ && cp -r /tmp/zig-linux-$(uname -m)-0.10.0-dev.2674+d980c6a38 /tmp/zig/
ENV PATH=/tmp/zig:$PATH
RUN cargo install cargo-zigbuild
ENV RUSTFLAGS="-C target-feature=-crt-static"
FROM rust_compile as scrypt
WORKDIR /usr/local/lib/php/extensions/
RUN \
git clone --depth 1 https://github.com/appwrite/php-scrypt.git && \
cd php-scrypt && \
cargo zigbuild --workspace --all-targets --target $(uname -m)-unknown-linux-musl --release && \
mv target/$(uname -m)-unknown-linux-musl/release/libphp_scrypt.so target/libphp_scrypt.so
FROM php:8.0.18-cli-alpine3.15 as final
LABEL maintainer="team@appwrite.io"
@ -193,8 +220,8 @@ ENV _APP_SERVER=swoole \
_APP_SMTP_SECURE= \
_APP_SMTP_USERNAME= \
_APP_SMTP_PASSWORD= \
_APP_PHONE_PROVIDER= \
_APP_PHONE_FROM= \
_APP_SMS_PROVIDER= \
_APP_SMS_FROM= \
_APP_FUNCTIONS_SIZE_LIMIT=30000000 \
_APP_FUNCTIONS_TIMEOUT=900 \
_APP_FUNCTIONS_CONTAINERS=10 \
@ -263,6 +290,7 @@ COPY --from=imagick /usr/local/lib/php/extensions/no-debug-non-zts-20200930/imag
COPY --from=yaml /usr/local/lib/php/extensions/no-debug-non-zts-20200930/yaml.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=maxmind /usr/local/lib/php/extensions/no-debug-non-zts-20200930/maxminddb.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=mongodb /usr/local/lib/php/extensions/no-debug-non-zts-20200930/mongodb.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
COPY --from=scrypt /usr/local/lib/php/extensions/php-scrypt/target/libphp_scrypt.so /usr/local/lib/php/extensions/no-debug-non-zts-20200930/
# Add Source Code
COPY ./app /usr/src/code/app
@ -319,6 +347,7 @@ RUN echo extension=redis.so >> /usr/local/etc/php/conf.d/redis.ini
RUN echo extension=imagick.so >> /usr/local/etc/php/conf.d/imagick.ini
RUN echo extension=yaml.so >> /usr/local/etc/php/conf.d/yaml.ini
RUN echo extension=maxminddb.so >> /usr/local/etc/php/conf.d/maxminddb.ini
RUN echo extension=libphp_scrypt.so >> /usr/local/etc/php/conf.d/libphp_scrypt.ini
RUN if [ "$DEBUG" == "true" ]; then printf "zend_extension=yasd \nyasd.debug_mode=remote \nyasd.init_file=/usr/local/dev/yasd_init.php \nyasd.remote_port=9005 \nyasd.log_level=-1" >> /usr/local/etc/php/conf.d/yasd.ini; fi
RUN if [ "$DEBUG" == "true" ]; then echo "opcache.enable=0" >> /usr/local/etc/php/conf.d/appwrite.ini; fi

View file

@ -1,5 +1,6 @@
<?php
use Appwrite\Auth\Auth;
use Utopia\Config\Config;
use Utopia\Database\Database;
@ -1158,8 +1159,30 @@ $collections = [
'required' => false,
'default' => null,
'array' => false,
'filters' => ['encrypt'],
],
[
'$id' => 'hash', // Hashing algorithm used to hash the password
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO,
'array' => false,
'filters' => [],
],
[
'$id' => 'hashOptions', // Configuration of hashing algorithm
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => Auth::DEFAULT_ALGO_OPTIONS,
'array' => false,
'filters' => ['json'],
],
[
'$id' => 'passwordUpdate',
'type' => Database::VAR_INTEGER,

View file

@ -50,7 +50,7 @@ return [
],
Exception::GENERAL_PHONE_DISABLED => [
'name' => Exception::GENERAL_PHONE_DISABLED,
'description' => 'Phone provider is not configured. Please check the _APP_PHONE_PROVIDER environment variable of your Appwrite server.',
'description' => 'Phone provider is not configured. Please check the _APP_SMS_PROVIDER environment variable of your Appwrite server.',
'code' => 503,
],
Exception::GENERAL_ARGUMENT_INVALID => [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -394,8 +394,8 @@ return [
'description' => '',
'variables' => [
[
'name' => '_APP_PHONE_PROVIDER',
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'phone://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic and telesign.",
'name' => '_APP_SMS_PROVIDER',
'description' => "Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. \n\nAvailable providers are twilio, text-magic and telesign.",
'introduction' => '0.15.0',
'default' => '',
'required' => false,
@ -403,7 +403,7 @@ return [
'filter' => ''
],
[
'name' => '_APP_PHONE_FROM',
'name' => '_APP_SMS_FROM',
'description' => 'Phone number used for sending out messages. Must start with a leading \'+\' and maximum of 15 digits without spaces (+123456789).',
'introduction' => '0.15.0',
'default' => '',

View file

@ -2,9 +2,9 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\Auth\Validator\Password;
use Appwrite\Auth\Validator\Phone as ValidatorPhone;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
@ -53,7 +53,7 @@ App::post('/v1/account')
->label('sdk.description', '/docs/references/account/create.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->label('abuse-limit', 10)
->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('email', '', new Email(), 'User email.')
@ -67,7 +67,6 @@ App::post('/v1/account')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $project, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
$email = \strtolower($email);
if ('console' === $project->getId()) {
$whitelistEmails = $project->getAttribute('authWhitelistEmails');
@ -101,7 +100,9 @@ App::post('/v1/account')
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => Auth::passwordHash($password),
'password' => Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'passwordUpdate' => \time(),
'registration' => \time(),
'reset' => false,
@ -129,7 +130,7 @@ App::post('/v1/account')
$events->setParam('userId', $user->getId());
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::post('/v1/account/sessions/email')
@ -166,8 +167,8 @@ App::post('/v1/account/sessions/email')
$profile = $dbForProject->findOne('users', [
new Query('email', Query::TYPE_EQUAL, [$email])]);
if (!$profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) {
throw new Exception(Exception::USER_INVALID_CREDENTIALS); // Wrong password or username
if (!$profile || !Auth::passwordVerify($password, $profile->getAttribute('password'), $profile->getAttribute('hash'), $profile->getAttribute('hashOptions'))) {
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
if (false === $profile->getAttribute('status')) { // Account is blocked
@ -198,6 +199,15 @@ App::post('/v1/account/sessions/email')
Authorization::setRole('user:' . $profile->getId());
// Re-hash if not using recommended algo
if ($profile->getAttribute('hash') !== Auth::DEFAULT_ALGO) {
$profile
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS);
$dbForProject->updateDocument('users', $profile->getId(), $profile);
}
$session = $dbForProject->createDocument('sessions', $session
->setAttribute('$read', ['user:' . $profile->getId()])
->setAttribute('$write', ['user:' . $profile->getId()]));
@ -483,7 +493,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'email' => $email,
'emailVerification' => true,
'status' => true, // Email should already be authenticated by OAuth2 provider
'password' => Auth::passwordHash(Auth::passwordGenerator()),
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'passwordUpdate' => 0,
'registration' => \time(),
'reset' => false,
@ -649,6 +661,8 @@ App::post('/v1/account/sessions/magic-url')
'emailVerification' => false,
'status' => true,
'password' => null,
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'passwordUpdate' => 0,
'registration' => \time(),
'reset' => false,
@ -849,7 +863,7 @@ App::post('/v1/account/sessions/phone')
->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. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->inject('request')
->inject('response')
->inject('project')
@ -857,9 +871,8 @@ App::post('/v1/account/sessions/phone')
->inject('audits')
->inject('events')
->inject('messaging')
->inject('phone')
->action(function (string $userId, string $number, Request $request, Response $response, Document $project, Database $dbForProject, Audit $audits, Event $events, EventPhone $messaging, Phone $phone) {
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
->action(function (string $userId, string $phone, Request $request, Response $response, Document $project, Database $dbForProject, Audit $audits, Event $events, EventPhone $messaging) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@ -867,7 +880,7 @@ App::post('/v1/account/sessions/phone')
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$user = $dbForProject->findOne('users', [new Query('phone', Query::TYPE_EQUAL, [$number])]);
$user = $dbForProject->findOne('users', [new Query('phone', Query::TYPE_EQUAL, [$phone])]);
if (!$user) {
$limit = $project->getAttribute('auths', [])['limit'] ?? 0;
@ -887,7 +900,7 @@ App::post('/v1/account/sessions/phone')
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'email' => null,
'phone' => $number,
'phone' => $phone,
'emailVerification' => false,
'phoneVerification' => false,
'status' => true,
@ -899,11 +912,11 @@ App::post('/v1/account/sessions/phone')
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $number])
'search' => implode(' ', [$userId, $phone])
])));
}
$secret = $phone->generateSecretDigits();
$secret = (App::getEnv('_APP_SMS_PROVIDER') === 'sms://mock') ? Mock::$digits : Auth::codeGenerator();
$expire = \time() + Auth::TOKEN_EXPIRATION_PHONE;
@ -927,7 +940,7 @@ App::post('/v1/account/sessions/phone')
$dbForProject->deleteCachedDocument('users', $user->getId());
$messaging
->setRecipient($number)
->setRecipient($phone)
->setMessage($secret)
->trigger();
@ -1118,6 +1131,8 @@ App::post('/v1/account/sessions/anonymous')
'emailVerification' => false,
'status' => true,
'password' => null,
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
'passwordUpdate' => 0,
'registration' => \time(),
'reset' => false,
@ -1248,15 +1263,13 @@ App::get('/v1/account')
->label('sdk.description', '/docs/references/account/get.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->inject('response')
->inject('user')
->inject('usage')
->action(function (Response $response, Document $user, Stats $usage) {
$usage->setParam('users.read', 1);
$response->dynamic($user, Response::MODEL_USER);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::get('/v1/account/prefs')
@ -1431,7 +1444,7 @@ App::patch('/v1/account/name')
->label('sdk.description', '/docs/references/account/update-name.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.')
->inject('response')
->inject('user')
@ -1453,7 +1466,7 @@ App::patch('/v1/account/name')
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::patch('/v1/account/password')
@ -1467,7 +1480,7 @@ App::patch('/v1/account/password')
->label('sdk.description', '/docs/references/account/update-password.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->param('password', '', new Password(), 'New user password. Must be at least 8 chars.')
->param('oldPassword', '', new Password(), 'Current user password. Must be at least 8 chars.', true)
->inject('response')
@ -1477,19 +1490,16 @@ App::patch('/v1/account/password')
->inject('usage')
->inject('events')
->action(function (string $password, string $oldPassword, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
// Check old password only if its an existing user.
if ($user->getAttribute('passwordUpdate') !== 0 && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password
if ($user->getAttribute('passwordUpdate') !== 0 && !Auth::passwordVerify($oldPassword, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
$user = $dbForProject->updateDocument(
'users',
$user->getId(),
$user
->setAttribute('password', Auth::passwordHash($password))
->setAttribute('passwordUpdate', \time())
);
$user = $dbForProject->updateDocument('users', $user->getId(), $user
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', \time()));
$audits
->setResource('user/' . $user->getId())
@ -1499,7 +1509,7 @@ App::patch('/v1/account/password')
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::patch('/v1/account/email')
@ -1513,7 +1523,7 @@ App::patch('/v1/account/email')
->label('sdk.description', '/docs/references/account/update-email.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->param('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('response')
@ -1523,12 +1533,11 @@ App::patch('/v1/account/email')
->inject('usage')
->inject('events')
->action(function (string $email, string $password, Response $response, Document $user, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
$isAnonymousUser = Auth::isAnonymousUser($user); // Check if request is from an anonymous account for converting
if (
!$isAnonymousUser &&
!Auth::passwordVerify($password, $user->getAttribute('password'))
!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))
) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
@ -1536,7 +1545,9 @@ App::patch('/v1/account/email')
$email = \strtolower($email);
$user
->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password) : $user->getAttribute('password', ''))
->setAttribute('password', $isAnonymousUser ? Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS) : $user->getAttribute('password', ''))
->setAttribute('hash', $isAnonymousUser ? Auth::DEFAULT_ALGO : $user->getAttribute('hash', ''))
->setAttribute('hashOptions', $isAnonymousUser ? Auth::DEFAULT_ALGO_OPTIONS : $user->getAttribute('hashOptions', ''))
->setAttribute('email', $email)
->setAttribute('emailVerification', false) // After this user needs to confirm mail again
->setAttribute('search', implode(' ', [$user->getId(), $user->getAttribute('name', ''), $email, $user->getAttribute('phone', '')]));
@ -1555,7 +1566,7 @@ App::patch('/v1/account/email')
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::patch('/v1/account/phone')
@ -1569,8 +1580,8 @@ App::patch('/v1/account/phone')
->label('sdk.description', '/docs/references/account/update-phone.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('number', '', new ValidatorPhone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->param('phone', '', new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->inject('response')
->inject('user')
@ -1584,7 +1595,7 @@ App::patch('/v1/account/phone')
if (
!$isAnonymousUser &&
!Auth::passwordVerify($password, $user->getAttribute('password'))
!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))
) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
@ -1608,7 +1619,7 @@ App::patch('/v1/account/phone')
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::patch('/v1/account/prefs')
@ -1622,7 +1633,7 @@ App::patch('/v1/account/prefs')
->label('sdk.description', '/docs/references/account/update-prefs.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->param('prefs', [], new Assoc(), 'Prefs key-value JSON object.')
->inject('response')
->inject('user')
@ -1638,7 +1649,7 @@ App::patch('/v1/account/prefs')
$usage->setParam('users.update', 1);
$events->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::patch('/v1/account/status')
@ -1652,7 +1663,7 @@ App::patch('/v1/account/status')
->label('sdk.description', '/docs/references/account/update-status.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->label('sdk.response.model', Response::MODEL_ACCOUNT)
->inject('request')
->inject('response')
->inject('user')
@ -1666,11 +1677,11 @@ App::patch('/v1/account/status')
$audits
->setResource('user/' . $user->getId())
->setPayload($response->output($user, Response::MODEL_USER));
->setPayload($response->output($user, Response::MODEL_ACCOUNT));
$events
->setParam('userId', $user->getId())
->setPayload($response->output($user, Response::MODEL_USER));
->setPayload($response->output($user, Response::MODEL_ACCOUNT));
if (!Config::getParam('domainVerification')) {
$response->addHeader('X-Fallback-Cookies', \json_encode([]));
@ -1678,7 +1689,7 @@ App::patch('/v1/account/status')
$usage->setParam('users.delete', 1);
$response->dynamic($user, Response::MODEL_USER);
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::delete('/v1/account/sessions/:sessionId')
@ -2041,7 +2052,6 @@ App::put('/v1/account/recovery')
->inject('usage')
->inject('events')
->action(function (string $userId, string $secret, string $password, string $passwordAgain, Response $response, Database $dbForProject, Audit $audits, Stats $usage, Event $events) {
if ($password !== $passwordAgain) {
throw new Exception(Exception::USER_PASSWORD_MISMATCH);
}
@ -2062,7 +2072,9 @@ App::put('/v1/account/recovery')
Authorization::setRole('user:' . $profile->getId());
$profile = $dbForProject->updateDocument('users', $profile->getId(), $profile
->setAttribute('password', Auth::passwordHash($password))
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', \time())
->setAttribute('emailVerification', true));
@ -2255,17 +2267,16 @@ App::post('/v1/account/verification/phone')
->label('abuse-key', 'userId:{userId}')
->inject('request')
->inject('response')
->inject('phone')
->inject('user')
->inject('dbForProject')
->inject('audits')
->inject('events')
->inject('usage')
->inject('messaging')
->action(function (Request $request, Response $response, Phone $phone, Document $user, Database $dbForProject, Audit $audits, Event $events, Stats $usage, EventPhone $messaging) {
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Audit $audits, Event $events, Stats $usage, EventPhone $messaging) {
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED);
}
if (empty($user->getAttribute('phone'))) {
@ -2278,7 +2289,7 @@ App::post('/v1/account/verification/phone')
$verificationSecret = Auth::tokenGenerator();
$secret = $phone->generateSecretDigits();
$secret = (App::getEnv('_APP_SMS_PROVIDER') === 'sms://mock') ? Mock::$digits : Auth::codeGenerator();
$expire = \time() + Auth::TOKEN_EXPIRATION_CONFIRM;
$verification = new Document([

View file

@ -531,7 +531,7 @@ App::delete('/v1/projects/:projectId')
->inject('deletes')
->action(function (string $projectId, string $password, Response $response, Document $user, Database $dbForConsole, Delete $deletes) {
if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password
if (!Auth::passwordVerify($password, $user->getAttribute('password'), $user->getAttribute('hash'), $user->getAttribute('hashOptions'))) { // Double check user password
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}

View file

@ -333,7 +333,9 @@ App::post('/v1/teams/:teamId/memberships')
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => Auth::passwordHash(Auth::passwordGenerator()),
'password' => Auth::passwordHash(Auth::passwordGenerator(), Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS),
'hash' => Auth::DEFAULT_ALGO,
'hashOptions' => Auth::DEFAULT_ALGO_OPTIONS,
/**
* Set the password update time to 0 for users created using
* team invite and OAuth to allow password updates without an

View file

@ -28,6 +28,56 @@ use Utopia\Validator\Text;
use Utopia\Validator\Range;
use Utopia\Validator\Boolean;
use MaxMind\Db\Reader;
use Utopia\Validator\Integer;
/** TODO: Remove function when we move to using utopia/platform */
function createUser(string $hash, mixed $hashOptions, string $userId, ?string $email, ?string $password, ?string $phone, string $name, Database $dbForProject, Stats $usage, Event $events): Document
{
$hashOptionsObject = (\is_string($hashOptions)) ? \json_decode($hashOptions, true) : $hashOptions; // Cast to JSON array
if (!empty($email)) {
$email = \strtolower($email);
}
try {
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
$user = $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'email' => $email,
'emailVerification' => false,
'phone' => $phone,
'phoneVerification' => false,
'status' => true,
'password' => (!empty($password)) ? ($hash === 'plaintext' ? Auth::passwordHash($password, $hash, $hashOptionsObject) : $password) : null,
'hash' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO : $hash,
'hashOptions' => $hash === 'plaintext' ? Auth::DEFAULT_ALGO_OPTIONS : $hashOptions,
'passwordUpdate' => (!empty($password)) ? \time() : 0,
'registration' => \time(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name])
]));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
$usage
->setParam('users.create', 1)
;
$events
->setParam('userId', $user->getId())
;
return $user;
}
App::post('/v1/users')
->desc('Create User')
@ -42,48 +92,228 @@ App::post('/v1/users')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User 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('email', null, new Email(), 'User email.', true)
->param('phone', null, new Phone(), 'Phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.', true)
->param('password', null, new Password(), 'Plain text user password. Must be at least 8 chars.', true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, ?string $email, ?string $phone, ?string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('plaintext', '{}', $userId, $email, $password, $phone, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/bcrypt')
->desc('Create User with Bcrypt Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createBcryptUser')
->label('sdk.description', '/docs/references/users/create-bcrypt-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User 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('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password. Must be at least 8 chars.')
->param('password', '', new Password(), 'User password hashed using Bcrypt.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('bcrypt', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$email = \strtolower($email);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
try {
$userId = $userId == 'unique()' ? $dbForProject->getId() : $userId;
$user = $dbForProject->createDocument('users', new Document([
'$id' => $userId,
'$read' => ['role:all'],
'$write' => ['user:' . $userId],
'email' => $email,
'emailVerification' => false,
'status' => true,
'password' => Auth::passwordHash($password),
'passwordUpdate' => \time(),
'registration' => \time(),
'reset' => false,
'name' => $name,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'search' => implode(' ', [$userId, $email, $name])
]));
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
App::post('/v1/users/md5')
->desc('Create User with MD5 Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createMD5User')
->label('sdk.description', '/docs/references/users/create-md5-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User 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('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using MD5.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('md5', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/argon2')
->desc('Create User with Argon2 Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createArgon2User')
->label('sdk.description', '/docs/references/users/create-argon2-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User 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('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Argon2.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('argon2', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/sha')
->desc('Create User with SHA Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createSHAUser')
->label('sdk.description', '/docs/references/users/create-sha-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User 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('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using SHA.')
->param('passwordVersion', '', new WhiteList(['sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512']), "Optional SHA version used to hash password. Allowed values are: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512/224', 'sha512/256', 'sha512', 'sha3-224', 'sha3-256', 'sha3-384', 'sha3-512'", true)
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordVersion, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$options = '{}';
if (!empty($passwordVersion)) {
$options = '{"version":"' . $passwordVersion . '"}';
}
$usage
->setParam('users.create', 1)
;
$user = createUser('sha', $options, $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$events
->setParam('userId', $user->getId())
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/phpass')
->desc('Create User with PHPass Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createPHPassUser')
->label('sdk.description', '/docs/references/users/create-phpass-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User 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('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using PHPass.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('phpass', '{}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/scrypt')
->desc('Create User with Scrypt Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createScryptUser')
->label('sdk.description', '/docs/references/users/create-scrypt-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User 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('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt.')
->param('passwordSalt', '', new Text(128), 'Optional salt used to hash password.')
->param('passwordCpu', '', new Integer(), 'Optional CPU cost used to hash password.')
->param('passwordMemory', '', new Integer(), 'Optional memory cost used to hash password.')
->param('passwordParallel', '', new Integer(), 'Optional parallelization cost used to hash password.')
->param('passwordLength', '', new Integer(), 'Optional hash length used to hash password.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordSalt, int $passwordCpu, int $passwordMemory, int $passwordParallel, int $passwordLength, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$options = [
'salt' => $passwordSalt,
'costCpu' => $passwordCpu,
'costMemory' => $passwordMemory,
'costParallel' => $passwordParallel,
'length' => $passwordLength
];
$user = createUser('scrypt', \json_encode($options), $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/scrypt-modified')
->desc('Create User with Scrypt Modified Password')
->groups(['api', 'users'])
->label('event', 'users.[userId].create')
->label('scope', 'users.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createScryptModifiedUser')
->label('sdk.description', '/docs/references/users/create-scrypt-modified-user.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USER)
->param('userId', '', new CustomId(), 'User 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('email', '', new Email(), 'User email.')
->param('password', '', new Password(), 'User password hashed using Scrypt Modified.')
->param('passwordSalt', '', new Text(128), 'Salt used to hash password.')
->param('passwordSaltSeparator', '', new Text(128), 'Salt separator used to hash password.')
->param('passwordSignerKey', '', new Text(128), 'Signer key used to hash password.')
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
->inject('response')
->inject('dbForProject')
->inject('usage')
->inject('events')
->action(function (string $userId, string $email, string $password, string $passwordSalt, string $passwordSaltSeparator, string $passwordSignerKey, string $name, Response $response, Database $dbForProject, Stats $usage, Event $events) {
$user = createUser('scryptMod', '{"signerKey":"' . $passwordSignerKey . '","saltSeparator":"' . $passwordSaltSeparator . '","salt":"' . $passwordSalt . '"}', $userId, $email, $password, null, $name, $dbForProject, $usage, $events);
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic($user, Response::MODEL_USER);
@ -541,7 +771,6 @@ App::patch('/v1/users/:userId/password')
->inject('audits')
->inject('events')
->action(function (string $userId, string $password, Response $response, Database $dbForProject, EventAudit $audits, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
@ -549,7 +778,9 @@ App::patch('/v1/users/:userId/password')
}
$user
->setAttribute('password', Auth::passwordHash($password))
->setAttribute('password', Auth::passwordHash($password, Auth::DEFAULT_ALGO, Auth::DEFAULT_ALGO_OPTIONS))
->setAttribute('hash', Auth::DEFAULT_ALGO)
->setAttribute('hashOptions', Auth::DEFAULT_ALGO_OPTIONS)
->setAttribute('passwordUpdate', \time());
$user = $dbForProject->updateDocument('users', $user->getId(), $user);

View file

@ -23,12 +23,12 @@ 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\Auth\Phone\Msg91;
use Appwrite\Auth\Phone\Vonage;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\SMS\Adapter\Telesign;
use Appwrite\SMS\Adapter\TextMagic;
use Appwrite\SMS\Adapter\Twilio;
use Appwrite\SMS\Adapter\Msg91;
use Appwrite\SMS\Adapter\Vonage;
use Appwrite\DSN\DSN;
use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
@ -982,8 +982,8 @@ App::setResource('geodb', function ($register) {
return $register->get('geodb');
}, ['register']);
App::setResource('phone', function () {
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
App::setResource('sms', function () {
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();

View file

@ -433,7 +433,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
$realtime->subscribe($project->getId(), $connection, $roles, $channels);
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_USER);
$user = empty($user->getId()) ? null : $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
'type' => 'connected',
@ -548,7 +548,7 @@ $server->onMessage(function (int $connection, string $message) use ($server, $re
$channels = Realtime::convertChannels(array_flip($realtime->connections[$connection]['channels']), $user->getId());
$realtime->subscribe($realtime->connections[$connection]['projectId'], $connection, $roles, $channels);
$user = $response->output($user, Response::MODEL_USER);
$user = $response->output($user, Response::MODEL_ACCOUNT);
$server->send([$connection], json_encode([
'type' => 'response',
'data' => [

View file

@ -149,8 +149,8 @@ services:
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_PHONE_PROVIDER
- _APP_PHONE_SECRET
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
appwrite-realtime:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>
@ -514,8 +514,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_PHONE_PROVIDER
- _APP_PHONE_FROM
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
@ -595,7 +595,7 @@ services:
- _APP_REDIS_PASS
mariadb:
image: mariadb:10.8.3 # fix issues when upgrading using: mysql_upgrade -u root -p
image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
restart: unless-stopped

View file

@ -1,12 +1,12 @@
<?php
use Appwrite\Auth\Phone;
use Appwrite\Auth\Phone\Mock;
use Appwrite\Auth\Phone\Telesign;
use Appwrite\Auth\Phone\TextMagic;
use Appwrite\Auth\Phone\Twilio;
use Appwrite\Auth\Phone\Msg91;
use Appwrite\Auth\Phone\Vonage;
use Appwrite\Auth\SMS;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\SMS\Adapter\Telesign;
use Appwrite\SMS\Adapter\TextMagic;
use Appwrite\SMS\Adapter\Twilio;
use Appwrite\SMS\Adapter\Msg91;
use Appwrite\SMS\Adapter\Vonage;
use Appwrite\DSN\DSN;
use Appwrite\Resque\Worker;
use Utopia\App;
@ -19,7 +19,7 @@ Console::success(APP_NAME . ' messaging worker v1 has started' . "\n");
class MessagingV1 extends Worker
{
protected ?Phone $phone = null;
protected ?SMS $sms = null;
protected ?string $from = null;
public function getName(): string
@ -29,11 +29,11 @@ class MessagingV1 extends Worker
public function init(): void
{
$dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER'));
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();
$this->phone = match ($dsn->getHost()) {
$this->sms = match ($dsn->getHost()) {
'mock' => new Mock('', ''), // used for tests
'twilio' => new Twilio($user, $secret),
'text-magic' => new TextMagic($user, $secret),
@ -43,12 +43,12 @@ class MessagingV1 extends Worker
default => null
};
$this->from = App::getEnv('_APP_PHONE_FROM');
$this->from = App::getEnv('_APP_SMS_FROM');
}
public function run(): void
{
if (empty(App::getEnv('_APP_PHONE_PROVIDER'))) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
Console::info('Skipped sms processing. No Phone provider has been set.');
return;
}
@ -62,7 +62,7 @@ class MessagingV1 extends Worker
$message = $this->args['message'];
try {
$this->phone->send($this->from, $recipient, $message);
$this->sms->send($this->from, $recipient, $message);
} catch (\Exception $error) {
throw new Exception('Error sending message: ' . $error->getMessage(), 500);
}

2
composer.lock generated
View file

@ -5370,5 +5370,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.2.0"
}

View file

@ -175,8 +175,8 @@ services:
- _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_PHONE_PROVIDER
- _APP_PHONE_SECRET
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
appwrite-realtime:
entrypoint: realtime
@ -545,8 +545,8 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_PHONE_PROVIDER
- _APP_PHONE_FROM
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
@ -638,7 +638,7 @@ services:
- _APP_REDIS_PASS
mariadb:
image: mariadb:10.8.3 # fix issues when upgrading using: mysql_upgrade -u root -p
image: mariadb:10.7 # fix issues when upgrading using: mysql_upgrade -u root -p
container_name: appwrite-mariadb
<<: *x-logging
networks:

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [Argon2](https://en.wikipedia.org/wiki/Argon2) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [MD5](https://en.wikipedia.org/wiki/MD5) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [PHPass](https://www.openwall.com/phpass/) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [Scrypt Modified](https://gist.github.com/Meldiron/eecf84a0225eccb5a378d45bb27462cc) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [Scrypt](https://github.com/Tarsnap/scrypt) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

View file

@ -0,0 +1 @@
Create a new user. Password provided must be hashed with the [SHA](https://en.wikipedia.org/wiki/Secure_Hash_Algorithm) algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint to create users with a plain text password.

2539
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -866,6 +866,79 @@ if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createArgon2User(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/argon2';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createBcryptUser(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/bcrypt';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createMD5User(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/md5';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createPHPassUser(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/phpass';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createScryptUser(userId,email,password,passwordSalt,passwordCpu,passwordMemory,passwordParallel,passwordLength,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
if(typeof passwordSalt==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSalt"');}
if(typeof passwordCpu==='undefined'){throw new AppwriteException('Missing required parameter: "passwordCpu"');}
if(typeof passwordMemory==='undefined'){throw new AppwriteException('Missing required parameter: "passwordMemory"');}
if(typeof passwordParallel==='undefined'){throw new AppwriteException('Missing required parameter: "passwordParallel"');}
if(typeof passwordLength==='undefined'){throw new AppwriteException('Missing required parameter: "passwordLength"');}
let path='/users/scrypt';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordSalt!=='undefined'){payload['passwordSalt']=passwordSalt;}
if(typeof passwordCpu!=='undefined'){payload['passwordCpu']=passwordCpu;}
if(typeof passwordMemory!=='undefined'){payload['passwordMemory']=passwordMemory;}
if(typeof passwordParallel!=='undefined'){payload['passwordParallel']=passwordParallel;}
if(typeof passwordLength!=='undefined'){payload['passwordLength']=passwordLength;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createScryptModifiedUser(userId,email,password,passwordSalt,passwordSaltSeparator,passwordSignerKey,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
if(typeof passwordSalt==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSalt"');}
if(typeof passwordSaltSeparator==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSaltSeparator"');}
if(typeof passwordSignerKey==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSignerKey"');}
let path='/users/scrypt-modified';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordSalt!=='undefined'){payload['passwordSalt']=passwordSalt;}
if(typeof passwordSaltSeparator!=='undefined'){payload['passwordSaltSeparator']=passwordSaltSeparator;}
if(typeof passwordSignerKey!=='undefined'){payload['passwordSignerKey']=passwordSignerKey;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createSHAUser(userId,email,password,passwordVersion,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/sha';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordVersion!=='undefined'){payload['passwordVersion']=passwordVersion;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getUsage(range,provider){return __awaiter(this,void 0,void 0,function*(){let path='/users/usage';let payload={};if(typeof range!=='undefined'){payload['range']=range;}
if(typeof provider!=='undefined'){payload['provider']=provider;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}

View file

@ -866,6 +866,79 @@ if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createArgon2User(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/argon2';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createBcryptUser(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/bcrypt';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createMD5User(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/md5';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createPHPassUser(userId,email,password,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/phpass';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createScryptUser(userId,email,password,passwordSalt,passwordCpu,passwordMemory,passwordParallel,passwordLength,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
if(typeof passwordSalt==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSalt"');}
if(typeof passwordCpu==='undefined'){throw new AppwriteException('Missing required parameter: "passwordCpu"');}
if(typeof passwordMemory==='undefined'){throw new AppwriteException('Missing required parameter: "passwordMemory"');}
if(typeof passwordParallel==='undefined'){throw new AppwriteException('Missing required parameter: "passwordParallel"');}
if(typeof passwordLength==='undefined'){throw new AppwriteException('Missing required parameter: "passwordLength"');}
let path='/users/scrypt';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordSalt!=='undefined'){payload['passwordSalt']=passwordSalt;}
if(typeof passwordCpu!=='undefined'){payload['passwordCpu']=passwordCpu;}
if(typeof passwordMemory!=='undefined'){payload['passwordMemory']=passwordMemory;}
if(typeof passwordParallel!=='undefined'){payload['passwordParallel']=passwordParallel;}
if(typeof passwordLength!=='undefined'){payload['passwordLength']=passwordLength;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createScryptModifiedUser(userId,email,password,passwordSalt,passwordSaltSeparator,passwordSignerKey,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
if(typeof passwordSalt==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSalt"');}
if(typeof passwordSaltSeparator==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSaltSeparator"');}
if(typeof passwordSignerKey==='undefined'){throw new AppwriteException('Missing required parameter: "passwordSignerKey"');}
let path='/users/scrypt-modified';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordSalt!=='undefined'){payload['passwordSalt']=passwordSalt;}
if(typeof passwordSaltSeparator!=='undefined'){payload['passwordSaltSeparator']=passwordSaltSeparator;}
if(typeof passwordSignerKey!=='undefined'){payload['passwordSignerKey']=passwordSignerKey;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
createSHAUser(userId,email,password,passwordVersion,name){return __awaiter(this,void 0,void 0,function*(){if(typeof userId==='undefined'){throw new AppwriteException('Missing required parameter: "userId"');}
if(typeof email==='undefined'){throw new AppwriteException('Missing required parameter: "email"');}
if(typeof password==='undefined'){throw new AppwriteException('Missing required parameter: "password"');}
let path='/users/sha';let payload={};if(typeof userId!=='undefined'){payload['userId']=userId;}
if(typeof email!=='undefined'){payload['email']=email;}
if(typeof password!=='undefined'){payload['password']=password;}
if(typeof passwordVersion!=='undefined'){payload['passwordVersion']=passwordVersion;}
if(typeof name!=='undefined'){payload['name']=name;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('post',uri,{'content-type':'application/json',},payload);});}
getUsage(range,provider){return __awaiter(this,void 0,void 0,function*(){let path='/users/usage';let payload={};if(typeof range!=='undefined'){payload['range']=range;}
if(typeof provider!=='undefined'){payload['provider']=provider;}
const uri=new URL(this.client.config.endpoint+path);return yield this.client.call('get',uri,{'content-type':'application/json',},payload);});}

View file

@ -695,7 +695,7 @@
* stored as is, and replaces any previous value. The maximum allowed prefs
* size is 64kB and throws error if exceeded.
*
* @param {Partial<Preferences>} prefs
* @param {object} prefs
* @throws {AppwriteException}
* @returns {Promise}
*/
@ -1459,7 +1459,8 @@
*
* You can use this endpoint to show different country flags icons to your
* users. The code argument receives the 2 letter country code. Use width,
* height and quality arguments to change the output settings.
* height and quality arguments to change the output settings. Country codes
* follow the [ISO 3166-1](http://en.wikipedia.org/wiki/ISO_3166-1) standard.
*
* When one dimension is specified and the other is 0, the image is scaled
* with preserved aspect ratio. If both dimensions are 0, the API provides an
@ -1860,7 +1861,7 @@
*
* Create a new Collection. Before using this route, you should create a new
* database resource using either a [server
* integration](/docs/server/database#databaseCreateCollection) API or
* integration](/docs/server/databases#databasesCreateCollection) API or
* directly from your database console.
*
* @param {string} databaseId
@ -2588,7 +2589,7 @@
*
* Create a new Document. Before using this route, you should create a new
* collection resource using either a [server
* integration](/docs/server/database#databaseCreateCollection) API or
* integration](/docs/server/databases#databasesCreateCollection) API or
* directly from your database console.
*
* @param {string} databaseId
@ -5289,8 +5290,8 @@
*
* Create a new file. Before using this route, you should create a new bucket
* resource using either a [server
* integration](/docs/server/database#storageCreateBucket) API or directly
* from your Appwrite console.
* integration](/docs/server/storage#storageCreateBucket) API or directly from
* your Appwrite console.
*
* Larger files should be uploaded using multiple requests with the
* [content-range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range)
@ -6186,6 +6187,388 @@
}, payload);
});
}
/**
* Create User with Argon2 Password
*
* Create a new user. Password provided must be hashed with the
* [Argon2](https://en.wikipedia.org/wiki/Argon2) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createArgon2User(userId, email, password, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/argon2';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with Bcrypt Password
*
* Create a new user. Password provided must be hashed with the
* [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createBcryptUser(userId, email, password, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/bcrypt';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with MD5 Password
*
* Create a new user. Password provided must be hashed with the
* [MD5](https://en.wikipedia.org/wiki/MD5) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createMD5User(userId, email, password, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/md5';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with PHPass Password
*
* Create a new user. Password provided must be hashed with the
* [PHPass](https://www.openwall.com/phpass/) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createPHPassUser(userId, email, password, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/phpass';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with Scrypt Password
*
* Create a new user. Password provided must be hashed with the
* [Scrypt](https://github.com/Tarsnap/scrypt) algorithm. Use the [POST
* /users](/docs/server/users#usersCreate) endpoint to create users with a
* plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} passwordSalt
* @param {number} passwordCpu
* @param {number} passwordMemory
* @param {number} passwordParallel
* @param {number} passwordLength
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createScryptUser(userId, email, password, passwordSalt, passwordCpu, passwordMemory, passwordParallel, passwordLength, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
if (typeof passwordSalt === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordSalt"');
}
if (typeof passwordCpu === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordCpu"');
}
if (typeof passwordMemory === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordMemory"');
}
if (typeof passwordParallel === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordParallel"');
}
if (typeof passwordLength === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordLength"');
}
let path = '/users/scrypt';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof passwordSalt !== 'undefined') {
payload['passwordSalt'] = passwordSalt;
}
if (typeof passwordCpu !== 'undefined') {
payload['passwordCpu'] = passwordCpu;
}
if (typeof passwordMemory !== 'undefined') {
payload['passwordMemory'] = passwordMemory;
}
if (typeof passwordParallel !== 'undefined') {
payload['passwordParallel'] = passwordParallel;
}
if (typeof passwordLength !== 'undefined') {
payload['passwordLength'] = passwordLength;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with Scrypt Modified Password
*
* Create a new user. Password provided must be hashed with the [Scrypt
* Modified](https://gist.github.com/Meldiron/eecf84a0225eccb5a378d45bb27462cc)
* algorithm. Use the [POST /users](/docs/server/users#usersCreate) endpoint
* to create users with a plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} passwordSalt
* @param {string} passwordSaltSeparator
* @param {string} passwordSignerKey
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createScryptModifiedUser(userId, email, password, passwordSalt, passwordSaltSeparator, passwordSignerKey, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
if (typeof passwordSalt === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordSalt"');
}
if (typeof passwordSaltSeparator === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordSaltSeparator"');
}
if (typeof passwordSignerKey === 'undefined') {
throw new AppwriteException('Missing required parameter: "passwordSignerKey"');
}
let path = '/users/scrypt-modified';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof passwordSalt !== 'undefined') {
payload['passwordSalt'] = passwordSalt;
}
if (typeof passwordSaltSeparator !== 'undefined') {
payload['passwordSaltSeparator'] = passwordSaltSeparator;
}
if (typeof passwordSignerKey !== 'undefined') {
payload['passwordSignerKey'] = passwordSignerKey;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Create User with SHA Password
*
* Create a new user. Password provided must be hashed with the
* [SHA](https://en.wikipedia.org/wiki/Secure_Hash_Algorithm) algorithm. Use
* the [POST /users](/docs/server/users#usersCreate) endpoint to create users
* with a plain text password.
*
* @param {string} userId
* @param {string} email
* @param {string} password
* @param {string} passwordVersion
* @param {string} name
* @throws {AppwriteException}
* @returns {Promise}
*/
createSHAUser(userId, email, password, passwordVersion, name) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}
if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}
if (typeof password === 'undefined') {
throw new AppwriteException('Missing required parameter: "password"');
}
let path = '/users/sha';
let payload = {};
if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}
if (typeof email !== 'undefined') {
payload['email'] = email;
}
if (typeof password !== 'undefined') {
payload['password'] = password;
}
if (typeof passwordVersion !== 'undefined') {
payload['passwordVersion'] = passwordVersion;
}
if (typeof name !== 'undefined') {
payload['name'] = name;
}
const uri = new URL(this.client.config.endpoint + path);
return yield this.client.call('post', uri, {
'content-type': 'application/json',
}, payload);
});
}
/**
* Get usage stats for the users API
*

View file

@ -2,11 +2,32 @@
namespace Appwrite\Auth;
use Appwrite\Auth\Hash\Argon2;
use Appwrite\Auth\Hash\Bcrypt;
use Appwrite\Auth\Hash\Md5;
use Appwrite\Auth\Hash\Phpass;
use Appwrite\Auth\Hash\Scrypt;
use Appwrite\Auth\Hash\Scryptmodified;
use Appwrite\Auth\Hash\Sha;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
class Auth
{
public const SUPPORTED_ALGOS = [
'argon2',
'bcrypt',
'md5',
'sha',
'phpass',
'scrypt',
'scryptMod',
'plaintext'
];
public const DEFAULT_ALGO = 'argon2';
public const DEFAULT_ALGO_OPTIONS = ['memoryCost' => 2048, 'timeCost' => 4, 'threads' => 3];
/**
* User Roles.
*/
@ -133,26 +154,98 @@ class Auth
*
* One way string hashing for user passwords
*
* @param $string
* @param string $string
* @param string $algo hashing algorithm to use
* @param array $options algo-specific options
*
* @return bool|string|null
*/
public static function passwordHash($string)
public static function passwordHash(string $string, string $algo, array $options = [])
{
return \password_hash($string, PASSWORD_BCRYPT, array('cost' => 8));
// Plain text not supported, just an alias. Switch to recommended algo
if ($algo === 'plaintext') {
$algo = Auth::DEFAULT_ALGO;
$options = Auth::DEFAULT_ALGO_OPTIONS;
}
if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) {
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
switch ($algo) {
case 'argon2':
$hasher = new Argon2($options);
return $hasher->hash($string);
case 'bcrypt':
$hasher = new Bcrypt($options);
return $hasher->hash($string);
case 'md5':
$hasher = new Md5($options);
return $hasher->hash($string);
case 'sha':
$hasher = new Sha($options);
return $hasher->hash($string);
case 'phpass':
$hasher = new Phpass($options);
return $hasher->hash($string);
case 'scrypt':
$hasher = new Scrypt($options);
return $hasher->hash($string);
case 'scryptMod':
$hasher = new Scryptmodified($options);
return $hasher->hash($string);
default:
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
}
/**
* Password verify.
*
* @param $plain
* @param $hash
* @param string $plain
* @param string $hash
* @param string $algo hashing algorithm used to hash
* @param array $options algo-specific options
*
* @return bool
*/
public static function passwordVerify($plain, $hash)
public static function passwordVerify(string $plain, string $hash, string $algo, array $options = [])
{
return \password_verify($plain, $hash);
// Plain text not supported, just an alias. Switch to recommended algo
if ($algo === 'plaintext') {
$algo = Auth::DEFAULT_ALGO;
$options = Auth::DEFAULT_ALGO_OPTIONS;
}
if (!\in_array($algo, Auth::SUPPORTED_ALGOS)) {
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
switch ($algo) {
case 'argon2':
$hasher = new Argon2($options);
return $hasher->verify($plain, $hash);
case 'bcrypt':
$hasher = new Bcrypt($options);
return $hasher->verify($plain, $hash);
case 'md5':
$hasher = new Md5($options);
return $hasher->verify($plain, $hash);
case 'sha':
$hasher = new Sha($options);
return $hasher->verify($plain, $hash);
case 'phpass':
$hasher = new Phpass($options);
return $hasher->verify($plain, $hash);
case 'scrypt':
$hasher = new Scrypt($options);
return $hasher->verify($plain, $hash);
case 'scryptMod':
$hasher = new Scryptmodified($options);
return $hasher->verify($plain, $hash);
default:
throw new \Exception('Hashing algorithm \'' . $algo . '\' is not supported.');
}
}
/**
@ -163,8 +256,6 @@ class Auth
* @param int $length
*
* @return string
*
* @throws \Exception
*/
public static function passwordGenerator(int $length = 20): string
{
@ -179,14 +270,32 @@ class Auth
* @param int $length
*
* @return string
*
* @throws \Exception
*/
public static function tokenGenerator(int $length = 128): string
{
return \bin2hex(\random_bytes($length));
}
/**
* Code Generator.
*
* Generate random code string
*
* @param int $length
*
* @return string
*/
public static function codeGenerator(int $length = 6): string
{
$value = '';
for ($i = 0; $i < $length; $i++) {
$value .= random_int(0, 9);
}
return $value;
}
/**
* Verify token and check that its not expired.
*

View file

@ -0,0 +1,62 @@
<?php
namespace Appwrite\Auth;
abstract class Hash
{
/**
* @var array $options Hashing-algo specific options
*/
protected array $options = [];
/**
* @param array $options Hashing-algo specific options
*/
public function __construct(array $options = [])
{
$this->setOptions($options);
}
/**
* Set hashing algo options
*
* @param array $options Hashing-algo specific options
*/
public function setOptions(array $options): self
{
$this->options = \array_merge([], $this->getDefaultOptions(), $options);
return $this;
}
/**
* Get hashing algo options
*
* @return array $options Hashing-algo specific options
*/
public function getOptions(): array
{
return $this->options;
}
/**
* @param string $password Input password to hash
*
* @return string hash
*/
abstract public function hash(string $password): string;
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
abstract public function verify(string $password, string $hash): bool;
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
abstract public function getDefaultOptions(): array;
}

View file

@ -0,0 +1,47 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* Argon2 accepted options:
* int threads
* int time_cost
* int memory_cost
*
* Reference: https://www.php.net/manual/en/function.password-hash.php#example-983
*/
class Argon2 extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
return \password_hash($password, PASSWORD_ARGON2ID, $this->getOptions());
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return \password_verify($password, $hash);
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3];
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* Bcrypt accepted options:
* int cost
* string? salt; auto-generated if empty
*
* Reference: https://www.php.net/manual/en/password.constants.php
*/
class Bcrypt extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
return \password_hash($password, PASSWORD_BCRYPT, $this->getOptions());
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return \password_verify($password, $hash);
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [ 'cost' => 8 ];
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* MD5 does not accept any options.
*
* Reference: https://www.php.net/manual/en/function.md5.php
*/
class Md5 extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
return \md5($password);
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return $this->hash($password) === $hash;
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [];
}
}

View file

@ -0,0 +1,290 @@
<?php
/**
* Portable PHP password hashing framework.
* source Version 0.5 / genuine.
* Written by Solar Designer <solar at openwall.com> in 2004-2017 and placed in
* the public domain. Revised in subsequent years, still public domain.
* There's absolutely no warranty.
* The homepage URL for the source framework is: http://www.openwall.com/phpass/
* Please be sure to update the Version line if you edit this file in any way.
* It is suggested that you leave the main version number intact, but indicate
* your project name (after the slash) and add your own revision information.
* Please do not change the "private" password hashing method implemented in
* here, thereby making your hashes incompatible. However, if you must, please
* change the hash type identifier (the "$P$") to something different.
* Obviously, since this code is in the public domain, the above are not
* requirements (there can be none), but merely suggestions.
*
* @author Solar Designer <solar@openwall.com>
* @copyright Copyright (C) 2017 All rights reserved.
* @license http://www.opensource.org/licenses/mit-license.html MIT License; see LICENSE.txt
*/
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* PHPass accepted options:
* int iteration_count_log2; The Logarithmic cost value used when generating hash values indicating the number of rounds used to generate hashes
* string portable_hashes
* string random_state; The cached random state
*
* Reference: https://github.com/photodude/phpass
*/
class Phpass extends Hash
{
/**
* Alphabet used in itoa64 conversions.
*
* @var string
* @since 0.1.0
*/
protected string $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
$randomState = \microtime();
if (\function_exists('getmypid')) {
$randomState .= getmypid();
}
return ['iteration_count_log2' => 8, 'portable_hashes' => false, 'random_state' => $randomState];
}
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
$options = $this->getDefaultOptions();
$random = '';
if (CRYPT_BLOWFISH === 1 && !$options['portable_hashes']) {
$random = $this->getRandomBytes(16, $options);
$hash = crypt($password, $this->gensaltBlowfish($random, $options));
if (strlen($hash) === 60) {
return $hash;
}
}
if (strlen($random) < 6) {
$random = $this->getRandomBytes(6, $options);
}
$hash = $this->cryptPrivate($password, $this->gensaltPrivate($random, $options));
if (strlen($hash) === 34) {
return $hash;
}
/**
* Returning '*' on error is safe here, but would _not_ be safe
* in a crypt(3)-like function used _both_ for generating new
* hashes and for validating passwords against existing hashes.
*/
return '*';
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
$verificationHash = $this->cryptPrivate($password, $hash);
if ($verificationHash[0] === '*') {
$verificationHash = crypt($password, $hash);
}
/**
* This is not constant-time. In order to keep the code simple,
* for timing safety we currently rely on the salts being
* unpredictable, which they are at least in the non-fallback
* cases (that is, when we use /dev/urandom and bcrypt).
*/
return $hash === $verificationHash;
}
/**
* @param int $count
*
* @return String $output
* @since 0.1.0
* @throws Exception Thows an Exception if the $count parameter is not a positive integer.
*/
protected function getRandomBytes(int $count, array $options): string
{
if (!is_int($count) || $count < 1) {
throw new \Exception('Argument count must be a positive integer');
}
$output = '';
if (@is_readable('/dev/urandom') && ($fh = @fopen('/dev/urandom', 'rb'))) {
$output = fread($fh, $count);
fclose($fh);
}
if (strlen($output) < $count) {
$output = '';
for ($i = 0; $i < $count; $i += 16) {
$options['iteration_count_log2'] = md5(microtime() . $options['iteration_count_log2']);
$output .= md5($options['iteration_count_log2'], true);
}
$output = substr($output, 0, $count);
}
return $output;
}
/**
* @param String $input
* @param int $count
*
* @return String $output
* @since 0.1.0
* @throws Exception Thows an Exception if the $count parameter is not a positive integer.
*/
protected function encode64($input, $count)
{
if (!is_int($count) || $count < 1) {
throw new \Exception('Argument count must be a positive integer');
}
$output = '';
$i = 0;
do {
$value = ord($input[$i++]);
$output .= $this->itoa64[$value & 0x3f];
if ($i < $count) {
$value |= ord($input[$i]) << 8;
}
$output .= $this->itoa64[($value >> 6) & 0x3f];
if ($i++ >= $count) {
break;
}
if ($i < $count) {
$value |= ord($input[$i]) << 16;
}
$output .= $this->itoa64[($value >> 12) & 0x3f];
if ($i++ >= $count) {
break;
}
$output .= $this->itoa64[($value >> 18) & 0x3f];
} while ($i < $count);
return $output;
}
/**
* @param String $input
*
* @return String $output
* @since 0.1.0
*/
private function gensaltPrivate($input, $options)
{
$output = '$P$';
$output .= $this->itoa64[min($options['iteration_count_log2'] + ((PHP_VERSION >= '5') ? 5 : 3), 30)];
$output .= $this->encode64($input, 6);
return $output;
}
/**
* @param String $password
* @param String $setting
*
* @return String $output
* @since 0.1.0
*/
private function cryptPrivate($password, $setting)
{
$output = '*0';
if (substr($setting, 0, 2) === $output) {
$output = '*1';
}
$id = substr($setting, 0, 3);
// We use "$P$", phpBB3 uses "$H$" for the same thing
if ($id !== '$P$' && $id !== '$H$') {
return $output;
}
$count_log2 = strpos($this->itoa64, $setting[3]);
if ($count_log2 < 7 || $count_log2 > 30) {
return $output;
}
$count = 1 << $count_log2;
$salt = substr($setting, 4, 8);
if (strlen($salt) !== 8) {
return $output;
}
/**
* We were kind of forced to use MD5 here since it's the only
* cryptographic primitive that was available in all versions of PHP
* in use. To implement our own low-level crypto in PHP
* would have result in much worse performance and
* consequently in lower iteration counts and hashes that are
* quicker to crack (by non-PHP code).
*/
$hash = md5($salt . $password, true);
do {
$hash = md5($hash . $password, true);
} while (--$count);
$output = substr($setting, 0, 12);
$output .= $this->encode64($hash, 16);
return $output;
}
/**
* @param String $input
*
* @return String $output
* @since 0.1.0
*/
private function gensaltBlowfish($input, $options)
{
/**
* This one needs to use a different order of characters and a
* different encoding scheme from the one in encode64() above.
* We care because the last character in our encoded string will
* only represent 2 bits. While two known implementations of
* bcrypt will happily accept and correct a salt string which
* has the 4 unused bits set to non-zero, we do not want to take
* chances and we also do not want to waste an additional byte
* of entropy.
*/
$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$output = '$2a$';
$output .= chr(ord('0') + $options['iteration_count_log2'] / 10);
$output .= chr(ord('0') + $options['iteration_count_log2'] % 10);
$output .= '$';
$i = 0;
do {
$c1 = ord($input[$i++]);
$output .= $itoa64[$c1 >> 2];
$c1 = ($c1 & 0x03) << 4;
if ($i >= 16) {
$output .= $itoa64[$c1];
break;
}
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 4;
$output .= $itoa64[$c1];
$c1 = ($c2 & 0x0f) << 2;
$c2 = ord($input[$i++]);
$c1 |= $c2 >> 6;
$output .= $itoa64[$c1];
$output .= $itoa64[$c2 & 0x3f];
} while (1);
return $output;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* Scrypt accepted options:
* string? salt; auto-generated if empty
* int costCpu
* int costMemory
* int costParallel
* int length
*
* Reference: https://github.com/DomBlack/php-scrypt/blob/master/scrypt.php#L112-L116
*/
class Scrypt extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
$options = $this->getOptions();
return \scrypt($password, $options['salt'], $options['costCpu'], $options['costMemory'], $options['costParallel'], $options['length']);
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return $hash === $this->hash($password);
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [ 'costCpu' => 8, 'costMemory' => 14, 'costParallel' => 1, 'length' => 64 ];
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* This is Scrypt hash with some additional steps added by Google.
*
* string salt
* string saltSeparator
* strin signerKey
*
* Reference: https://github.com/DomBlack/php-scrypt/blob/master/scrypt.php#L112-L116
*/
class Scryptmodified extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
$options = $this->getOptions();
$derivedKeyBytes = $this->generateDerivedKey($password);
$signerKeyBytes = \base64_decode($options['signerKey']);
$hashedPassword = $this->hashKeys($signerKeyBytes, $derivedKeyBytes);
return \base64_encode($hashedPassword);
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return $this->hash($password) === $hash;
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [ ];
}
private function generateDerivedKey(string $password)
{
$options = $this->getOptions();
$saltBytes = \base64_decode($options['salt']);
$saltSeparatorBytes = \base64_decode($options['saltSeparator']);
$derivedKey = \scrypt(\utf8_encode($password), $saltBytes . $saltSeparatorBytes, 16384, 8, 1, 64);
$derivedKey = \hex2bin($derivedKey);
return $derivedKey;
}
private function hashKeys($signerKeyBytes, $derivedKeyBytes): string
{
$key = \substr($derivedKeyBytes, 0, 32);
$iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
$hash = \openssl_encrypt($signerKeyBytes, 'aes-256-ctr', $key, OPENSSL_RAW_DATA, $iv);
return $hash;
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Appwrite\Auth\Hash;
use Appwrite\Auth\Hash;
/*
* SHA accepted options:
* string? version. Allowed:
* - Version 1: sha1
* - Version 2: sha224, sha256, sha384, sha512/224, sha512/256, sha512
* - Version 3: sha3-224, sha3-256, sha3-384, sha3-512
*
* Reference: https://www.php.net/manual/en/function.hash-algos.php
*/
class Sha extends Hash
{
/**
* @param string $password Input password to hash
*
* @return string hash
*/
public function hash(string $password): string
{
$algo = $this->getOptions()['version'];
return \hash($algo, $password);
}
/**
* @param string $password Input password to validate
* @param string $hash Hash to verify password against
*
* @return boolean true if password matches hash
*/
public function verify(string $password, string $hash): bool
{
return $this->hash($password) === $hash;
}
/**
* Get default options for specific hashing algo
*
* @return array options named array
*/
public function getDefaultOptions(): array
{
return [ 'version' => 'sha3-512' ];
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace Appwrite\Auth\Phone;
use Appwrite\Auth\Phone;
class Mock extends Phone
{
/**
* @var string
*/
public static 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

@ -1,10 +1,8 @@
<?php
namespace Appwrite\Auth;
namespace Appwrite\SMS;
use Appwrite\Extend\Exception;
abstract class Phone
abstract class Adapter
{
/**
* @var string
@ -69,20 +67,9 @@ abstract class Phone
\curl_close($ch);
if ($code >= 400) {
throw new Exception($response);
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,24 @@
<?php
namespace Appwrite\SMS\Adapter;
use Appwrite\SMS\Adapter;
class Mock extends Adapter
{
/**
* @var string
*/
public static string $digits = '123456';
/**
* @param string $from
* @param string $to
* @param string $message
* @return void
*/
public function send(string $from, string $to, string $message): void
{
return;
}
}

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://docs.msg91.com/p/tf9GTextN/e/Irz7-x1PK/MSG91
class Msg91 extends Phone
class Msg91 extends Adapter
{
/**
* @var string
@ -16,10 +16,10 @@ class Msg91 extends Phone
/**
* For Flow based sending SMS sender ID should not be set in flow
* In environment _APP_PHONE_PROVIDER format is 'phone://[senderID]:[authKey]@msg91'.
* _APP_PHONE_FROM value is flow ID created in Msg91
* Eg. _APP_PHONE_PROVIDER = phone://DINESH:5e1e93cad6fc054d8e759a5b@msg91
* _APP_PHONE_FROM = 3968636f704b303135323339
* In environment _APP_SMS_PROVIDER format is 'sms://[senderID]:[authKey]@msg91'.
* _APP_SMS_FROM value is flow ID created in Msg91
* Eg. _APP_SMS_PROVIDER = sms://DINESH:5e1e93cad6fc054d8e759a5b@msg91
* _APP_SMS_FROM = 3968636f704b303135323339
* @param string $from-> utilized from for flow id
* @param string $to
* @param string $message

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://developer.telesign.com/enterprise/docs/sms-api-send-an-sms
class Telesign extends Phone
class Telesign extends Adapter
{
/**
* @var string

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://www.textmagic.com/docs/api/start/
class TextMagic extends Phone
class TextMagic extends Adapter
{
/**
* @var string

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://www.twilio.com/docs/sms/api
class Twilio extends Phone
class Twilio extends Adapter
{
/**
* @var string

View file

@ -1,13 +1,13 @@
<?php
namespace Appwrite\Auth\Phone;
namespace Appwrite\SMS\Adapter;
use Appwrite\Auth\Phone;
use Appwrite\SMS\Adapter;
// Reference Material
// https://developer.vonage.com/api/sms
class Vonage extends Phone
class Vonage extends Adapter
{
/**
* @var string

View file

@ -17,15 +17,32 @@ class OpenAPI3 extends Format
protected function getNestedModels(Model $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (
in_array($model->getType(), $usedModels)
&& !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double'])
) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
return;
if (!in_array($model->getType(), $usedModels)) {
continue;
}
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $ruleType) {
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $ruleType;
foreach ($this->models as $m) {
if ($m->getType() === $ruleType) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}
} else {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double'])) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}

View file

@ -17,15 +17,32 @@ class Swagger2 extends Format
protected function getNestedModels(Model $model, array &$usedModels): void
{
foreach ($model->getRules() as $rule) {
if (
in_array($model->getType(), $usedModels)
&& !in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float', 'double'])
) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
return;
if (!in_array($model->getType(), $usedModels)) {
continue;
}
if (\is_array($rule['type'])) {
foreach ($rule['type'] as $ruleType) {
if (!in_array($ruleType, ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $ruleType;
foreach ($this->models as $m) {
if ($m->getType() === $ruleType) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}
} else {
if (!in_array($rule['type'], ['string', 'integer', 'boolean', 'json', 'float'])) {
$usedModels[] = $rule['type'];
foreach ($this->models as $m) {
if ($m->getType() === $rule['type']) {
$this->getNestedModels($m, $usedModels);
continue;
}
}
}
}

View file

@ -8,6 +8,14 @@ use Swoole\Http\Response as SwooleHTTPResponse;
use Utopia\Database\Document;
use Appwrite\Utopia\Response\Filter;
use Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response\Model\Account;
use Appwrite\Utopia\Response\Model\AlgoArgon2;
use Appwrite\Utopia\Response\Model\AlgoBcrypt;
use Appwrite\Utopia\Response\Model\AlgoMd5;
use Appwrite\Utopia\Response\Model\AlgoPhpass;
use Appwrite\Utopia\Response\Model\AlgoScrypt;
use Appwrite\Utopia\Response\Model\AlgoScryptModified;
use Appwrite\Utopia\Response\Model\AlgoSha;
use Appwrite\Utopia\Response\Model\None;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
@ -118,6 +126,7 @@ class Response extends SwooleResponse
public const MODEL_ATTRIBUTE_URL = 'attributeUrl';
// Users
public const MODEL_ACCOUNT = 'account';
public const MODEL_USER = 'user';
public const MODEL_USER_LIST = 'userList';
public const MODEL_SESSION = 'session';
@ -126,6 +135,15 @@ class Response extends SwooleResponse
public const MODEL_JWT = 'jwt';
public const MODEL_PREFERENCES = 'preferences';
// Users password algos
public const MODEL_ALGO_MD5 = 'algoMd5';
public const MODEL_ALGO_SHA = 'algoSha';
public const MODEL_ALGO_SCRYPT = 'algoScrypt';
public const MODEL_ALGO_SCRYPT_MODIFIED = 'algoScryptModified';
public const MODEL_ALGO_BCRYPT = 'algoBcrypt';
public const MODEL_ALGO_ARGON2 = 'algoArgon2';
public const MODEL_ALGO_PHPASS = 'algoPhpass';
// Storage
public const MODEL_FILE = 'file';
public const MODEL_FILE_LIST = 'fileList';
@ -259,6 +277,14 @@ class Response extends SwooleResponse
->setModel(new ModelDocument())
->setModel(new Log())
->setModel(new User())
->setModel(new AlgoMd5())
->setModel(new AlgoSha())
->setModel(new AlgoPhpass())
->setModel(new AlgoBcrypt())
->setModel(new AlgoScrypt())
->setModel(new AlgoScryptModified())
->setModel(new AlgoArgon2())
->setModel(new Account())
->setModel(new Preferences())
->setModel(new Session())
->setModel(new Token())

View file

@ -91,6 +91,22 @@ abstract class Model
return $this;
}
/**
* Delete an existing Rule
* If rule exists, it will be removed
*
* @param string $key
* @param array $options
*/
protected function removeRule(string $key): self
{
if (isset($this->rules[$key])) {
unset($this->rules[$key]);
}
return $this;
}
public function getRequired()
{
$list = [];

View file

@ -0,0 +1,38 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class Account extends User
{
public function __construct()
{
parent::__construct();
$this
->removeRule('password')
->removeRule('hash')
->removeRule('hashOptions');
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Account';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ACCOUNT;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoArgon2 extends Model
{
public function __construct()
{
// No options if imported. If hashed by Appwrite, following configuration is available:
$this
->addRule('memoryCost', [
'type' => self::TYPE_INTEGER,
'description' => 'Memory used to compute hash.',
'default' => '',
'example' => 65536,
])
->addRule('timeCost', [
'type' => self::TYPE_INTEGER,
'description' => 'Amount of time consumed to compute hash',
'default' => '',
'example' => 4,
])
->addRule('threads', [
'type' => self::TYPE_INTEGER,
'description' => 'Number of threads used to compute hash.',
'default' => '',
'example' => 3,
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoArgon2';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_ARGON2;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoBcrypt extends Model
{
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoBcrypt';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_BCRYPT;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoMd5 extends Model
{
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoMD5';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_MD5;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoPhpass extends Model
{
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoPHPass';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_PHPASS;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoScrypt extends Model
{
public function __construct()
{
$this
->addRule('costCpu', [
'type' => self::TYPE_INTEGER,
'description' => 'CPU complexity of computed hash.',
'default' => '',
'example' => 8,
])
->addRule('costMemory', [
'type' => self::TYPE_INTEGER,
'description' => 'Memory complexity of computed hash.',
'default' => '',
'example' => 14,
])
->addRule('costParallel', [
'type' => self::TYPE_INTEGER,
'description' => 'Parallelization of computed hash.',
'default' => '',
'example' => 1,
])
->addRule('length', [
'type' => self::TYPE_INTEGER,
'description' => 'Length used to compute hash.',
'default' => '',
'example' => 1,
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoScrypt';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_SCRYPT;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoScryptModified extends Model
{
public function __construct()
{
$this
->addRule('salt', [
'type' => self::TYPE_STRING,
'description' => 'Salt used to compute hash.',
'default' => '',
'example' => 'UxLMreBr6tYyjQ==',
])
->addRule('saltSeparator', [
'type' => self::TYPE_STRING,
'description' => 'Separator used to compute hash.',
'default' => '',
'example' => 'Bw==',
])
->addRule('signerKey', [
'type' => self::TYPE_STRING,
'description' => 'Key used to compute hash.',
'default' => '',
'example' => 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoScryptModified';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_SCRYPT_MODIFIED;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AlgoSha extends Model
{
public function __construct()
{
// No options, because this can only be imported, and verifying doesnt require any configuration
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AlgoSHA';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_ALGO_SHA;
}
}

View file

@ -35,6 +35,33 @@ class User extends Model
'default' => '',
'example' => 'John Doe',
])
->addRule('password', [
'type' => self::TYPE_STRING,
'description' => 'Hashed user password.',
'default' => '',
'example' => '$argon2id$v=19$m=2048,t=4,p=3$aUZjLnliVWRINmFNTWMudg$5S+x+7uA31xFnrHFT47yFwcJeaP0w92L/4LdgrVRXxE',
])
->addRule('hash', [
'type' => self::TYPE_STRING,
'description' => 'Password hashing algorithm.',
'default' => '',
'example' => 'argon2',
])
->addRule('hashOptions', [
'type' => [
Response::MODEL_ALGO_ARGON2,
Response::MODEL_ALGO_SCRYPT,
Response::MODEL_ALGO_SCRYPT_MODIFIED,
Response::MODEL_ALGO_BCRYPT,
Response::MODEL_ALGO_PHPASS,
Response::MODEL_ALGO_SHA,
Response::MODEL_ALGO_MD5, // keep least secure at the bottom. this order will be used in docs
],
'description' => 'Password hashing algorithm configuration.',
'default' => [],
'example' => new \stdClass(),
'array' => false,
])
->addRule('registration', [
'type' => self::TYPE_INTEGER,
'description' => 'User registration date in Unix timestamp.',

View file

@ -2,7 +2,7 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Auth\Phone\Mock;
use Appwrite\SMS\Adapter\Mock;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
@ -690,7 +690,7 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => 'unique()',
'number' => $number,
'phone' => $number,
]);
$this->assertEquals(201, $response['headers']['status-code']);
@ -713,7 +713,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']);
$data['token'] = Mock::$defaultDigits;
$data['token'] = Mock::$digits;
$data['id'] = $userId;
$data['number'] = $number;
@ -869,7 +869,7 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'number' => $newPhone,
'phone' => $newPhone,
'password' => 'new-password'
]);
@ -949,7 +949,7 @@ class AccountCustomClientTest extends Scope
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'userId' => $id,
'secret' => Mock::$defaultDigits,
'secret' => Mock::$digits,
]);
$this->assertEquals(200, $response['headers']['status-code']);
@ -964,7 +964,7 @@ class AccountCustomClientTest extends Scope
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'userId' => 'ewewe',
'secret' => Mock::$defaultDigits,
'secret' => Mock::$digits,
]);
$this->assertEquals(404, $response['headers']['status-code']);

View file

@ -55,14 +55,313 @@ trait UsersBase
$this->assertEquals(true, $res['body']['status']);
$this->assertGreaterThan(0, $res['body']['registration']);
/**
* Test Create with hashed passwords
*/
$res = $this->client->call(Client::METHOD_POST, '/users/md5', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'md5',
'email' => 'md5@appwrite.io',
'password' => '144fa7eaa4904e8ee120651997f70dcc', // appwrite
'name' => 'MD5 User',
]);
$res = $this->client->call(Client::METHOD_POST, '/users/bcrypt', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'bcrypt',
'email' => 'bcrypt@appwrite.io',
'password' => '$2a$15$xX/myGbFU.ZSKHSi6EHdBOySTdYm8QxBLXmOPHrYMwV0mHRBBSBOq', // appwrite (15 rounds)
'name' => 'Bcrypt User',
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '$2a$15$xX/myGbFU.ZSKHSi6EHdBOySTdYm8QxBLXmOPHrYMwV0mHRBBSBOq');
$this->assertEquals($res['body']['hash'], 'bcrypt');
$res = $this->client->call(Client::METHOD_POST, '/users/argon2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'argon2',
'email' => 'argon2@appwrite.io',
'password' => '$argon2i$v=19$m=20,t=3,p=2$YXBwd3JpdGU$A/54i238ed09ZR4NwlACU5XnkjNBZU9QeOEuhjLiexI', // appwrite (salt appwrite, parallel 2, memory 20, iterations 3, length 32)
'name' => 'Argon2 User',
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '$argon2i$v=19$m=20,t=3,p=2$YXBwd3JpdGU$A/54i238ed09ZR4NwlACU5XnkjNBZU9QeOEuhjLiexI');
$this->assertEquals($res['body']['hash'], 'argon2');
$res = $this->client->call(Client::METHOD_POST, '/users/sha', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'sha512',
'email' => 'sha512@appwrite.io',
'password' => '4243da0a694e8a2f727c8060fe0507c8fa01ca68146c76d2c190805b638c20c6bf6ba04e21f11ae138785d0bff63c416e6f87badbffad37f6dee50094cc38c70', // appwrite (sha512)
'name' => 'SHA512 User',
'passwordVersion' => 'sha512'
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '4243da0a694e8a2f727c8060fe0507c8fa01ca68146c76d2c190805b638c20c6bf6ba04e21f11ae138785d0bff63c416e6f87badbffad37f6dee50094cc38c70');
$this->assertEquals($res['body']['hash'], 'sha');
$this->assertEquals($res['body']['hashOptions']['version'], 'sha512');
$res = $this->client->call(Client::METHOD_POST, '/users/scrypt', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'scrypt',
'email' => 'scrypt@appwrite.io',
'password' => '3fdef49701bc4cfaacd551fe017283513284b4731e6945c263246ef948d3cf63b5d269c31fd697246085111a428245e24a4ddc6b64c687bc60a8910dbafc1d5b', // appwrite (salt appwrite, cpu 16384, memory 13, parallel 2, length 64)
'name' => 'Scrypt User',
'passwordSalt' => 'appwrite',
'passwordCpu' => 16384,
'passwordMemory' => 13,
'passwordParallel' => 2,
'passwordLength' => 64
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '3fdef49701bc4cfaacd551fe017283513284b4731e6945c263246ef948d3cf63b5d269c31fd697246085111a428245e24a4ddc6b64c687bc60a8910dbafc1d5b');
$this->assertEquals($res['body']['hash'], 'scrypt');
$this->assertEquals($res['body']['hashOptions']['salt'], 'appwrite');
$this->assertEquals($res['body']['hashOptions']['costCpu'], 16384);
$this->assertEquals($res['body']['hashOptions']['costMemory'], 13);
$this->assertEquals($res['body']['hashOptions']['costParallel'], 2);
$this->assertEquals($res['body']['hashOptions']['length'], 64);
$res = $this->client->call(Client::METHOD_POST, '/users/phpass', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'phpass',
'email' => 'phpass@appwrite.io',
'password' => '$P$Br387rwferoKN7uwHZqNMu98q3U8RO.',
'name' => 'PHPass User',
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], '$P$Br387rwferoKN7uwHZqNMu98q3U8RO.');
$res = $this->client->call(Client::METHOD_POST, '/users/scrypt-modified', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'scrypt-modified',
'email' => 'scrypt-modified@appwrite.io',
'password' => 'UlM7JiXRcQhzAGlaonpSqNSLIz475WMddOgLjej5De9vxTy48K6WtqlEzrRFeK4t0COfMhWCb8wuMHgxOFCHFQ==', // appwrite
'name' => 'Scrypt Modified User',
'passwordSalt' => 'UxLMreBr6tYyjQ==',
'passwordSaltSeparator' => 'Bw==',
'passwordSignerKey' => 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==',
]);
$this->assertEquals($res['headers']['status-code'], 201);
$this->assertEquals($res['body']['password'], 'UlM7JiXRcQhzAGlaonpSqNSLIz475WMddOgLjej5De9vxTy48K6WtqlEzrRFeK4t0COfMhWCb8wuMHgxOFCHFQ==');
$this->assertEquals($res['body']['hash'], 'scryptMod');
$this->assertEquals($res['body']['hashOptions']['salt'], 'UxLMreBr6tYyjQ==');
$this->assertEquals($res['body']['hashOptions']['signerKey'], 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==');
$this->assertEquals($res['body']['hashOptions']['saltSeparator'], 'Bw==');
return ['userId' => $body['$id']];
}
/**
* Tries to login into all accounts created with hashed password. Ensures hash veifying logic.
*
* @depends testCreateUser
*/
public function testCreateUserSessionHashed(array $data): void
{
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'md5@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'md5');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'bcrypt@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'bcrypt');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'argon2@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'argon2');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'sha512@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'sha512');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'scrypt@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'scrypt');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'phpass@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'phpass');
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => 'scrypt-modified@appwrite.io',
'password' => 'appwrite',
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertEquals($response['body']['userId'], 'scrypt-modified');
}
/**
* Tests all optional parameters of createUser (email, phone, anonymous..)
*
* @depends testCreateUser
*/
public function testCreateUserTypes(array $data): void
{
/**
* Test for SUCCESS
*/
// Email + password
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'email' => 'emailuser@appwrite.io',
'password' => 'emailUserPassword',
]);
$this->assertNotEmpty($response['body']['email']);
$this->assertNotEmpty($response['body']['password']);
$this->assertEmpty($response['body']['phone']);
// Phone
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'phone' => '+123456789012',
]);
$this->assertEmpty($response['body']['email']);
$this->assertEmpty($response['body']['password']);
$this->assertNotEmpty($response['body']['phone']);
// Anonymous
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
]);
$this->assertEmpty($response['body']['email']);
$this->assertEmpty($response['body']['password']);
$this->assertEmpty($response['body']['phone']);
// Email-only
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'email' => 'emailonlyuser@appwrite.io',
]);
$this->assertNotEmpty($response['body']['email']);
$this->assertEmpty($response['body']['password']);
$this->assertEmpty($response['body']['phone']);
// Password-only
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'password' => 'passwordOnlyUser',
]);
$this->assertEmpty($response['body']['email']);
$this->assertNotEmpty($response['body']['password']);
$this->assertEmpty($response['body']['phone']);
// Password and phone
$response = $this->client->call(Client::METHOD_POST, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'userId' => 'unique()',
'password' => 'passwordOnlyUser',
'phone' => '+123456789013',
]);
$this->assertEmpty($response['body']['email']);
$this->assertNotEmpty($response['body']['password']);
$this->assertNotEmpty($response['body']['phone']);
}
/**
* @depends testCreateUser
*/
public function testListUsers(array $data): void
{
$totalUsers = 15;
/**
* Test for SUCCESS listUsers
*/
@ -74,7 +373,7 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(2, $response['body']['users']);
$this->assertCount($totalUsers, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], $data['userId']);
$this->assertEquals($response['body']['users'][1]['$id'], 'user1');
@ -89,7 +388,7 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['users']);
$this->assertCount(1, $response['body']['users']);
$this->assertCount($totalUsers - 1, $response['body']['users']);
$this->assertEquals($response['body']['users'][0]['$id'], 'user1');
$response = $this->client->call(Client::METHOD_GET, '/users', array_merge([

View file

@ -44,12 +44,137 @@ class AuthTest extends TestCase
public function testPassword(): void
{
$secret = 'secret';
$static = '$2y$08$PDbMtV18J1KOBI9tIYabBuyUwBrtXPGhLxCy9pWP6xkldVOKLrLKy';
$dynamic = Auth::passwordHash($secret);
/*
General tests, using pre-defined hashes generated by online tools
*/
$this->assertEquals(Auth::passwordVerify($secret, $dynamic), true);
$this->assertEquals(Auth::passwordVerify($secret, $static), true);
// Bcrypt - Version Y
$plain = 'secret';
$hash = '$2y$08$PDbMtV18J1KOBI9tIYabBuyUwBrtXPGhLxCy9pWP6xkldVOKLrLKy';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// Bcrypt - Version A
$plain = 'test123';
$hash = '$2a$12$3f2ZaARQ1AmhtQWx2nmQpuXcWfTj1YV2/Hl54e8uKxIzJe3IfwLiu';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// Bcrypt - Cost 5
$plain = 'hello-world';
$hash = '$2a$05$IjrtSz6SN7UJ6Sh3l.b5jODEvEG2LMJTPAHIaLWRvlWx7if3VMkFO';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// Bcrypt - Cost 15
$plain = 'super-secret-password';
$hash = '$2a$15$DS0ZzbsFZYumH/E4Qj5oeOHnBcM3nCCsCA2m4Goigat/0iMVQC4Na';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// MD5 - Short
$plain = 'appwrite';
$hash = '144fa7eaa4904e8ee120651997f70dcc';
$generatedHash = Auth::passwordHash($plain, 'md5');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
// MD5 - Long
$plain = 'AppwriteIsAwesomeBackendAsAServiceThatIsAlsoOpenSourced';
$hash = '8410e96cf7ac64e0b84c3f8517a82616';
$generatedHash = Auth::passwordHash($plain, 'md5');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md5'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'md5'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'md5'));
// PHPass
$plain = 'pass123';
$hash = '$P$BVKPmJBZuLch27D4oiMRTEykGLQ9tX0';
$generatedHash = Auth::passwordHash($plain, 'phpass');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
// SHA
$plain = 'developersAreAwesome!';
$hash = '2455118438cb125354b89bb5888346e9bd23355462c40df393fab514bf2220b5a08e4e2d7b85d7327595a450d0ac965cc6661152a46a157c66d681bed20a4735';
$generatedHash = Auth::passwordHash($plain, 'sha');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'sha'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'sha'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'sha'));
// Argon2
$plain = 'safe-argon-password';
$hash = '$argon2id$v=19$m=2048,t=3,p=4$MWc5NWRmc2QxZzU2$41mp7rSgBZ49YxLbbxIac7aRaxfp5/e1G45ckwnK0g8';
$generatedHash = Auth::passwordHash($plain, 'argon2');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'argon2'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'argon2'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'argon2'));
// Scrypt
$plain = 'some-scrypt-password';
$hash = 'b448ad7ba88b653b5b56b8053a06806724932d0751988bc9cd0ef7ff059e8ba8a020e1913b7069a650d3f99a1559aba0221f2c277826919513a054e76e339028';
$generatedHash = Auth::passwordHash($plain, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
$this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-wrong-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
$this->assertEquals(false, Auth::passwordVerify($plain, $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 10, 'costParallel' => 2]));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scrypt', [ 'salt' => 'some-salt', 'length' => 64, 'costCpu' => 16384, 'costMemory' => 12, 'costParallel' => 2]));
// ScryptModified tested are in provider-specific tests below
/*
Provider-specific tests, ensuring functionality of specific use-cases
*/
// Provider #1 (Database)
$plain = 'example-password';
$hash = '$2a$10$3bIGRWUes86CICsuchGLj.e.BqdCdg2/1Ud9LvBhJr0j7Dze8PBdS';
$generatedHash = Auth::passwordHash($plain, 'bcrypt');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'bcrypt'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'bcrypt'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'bcrypt'));
// Provider #2 (Blog)
$plain = 'your-password';
$hash = '$P$BkiNDJTpAWXtpaMhEUhUdrv7M0I1g6.';
$generatedHash = Auth::passwordHash($plain, 'phpass');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'phpass'));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'phpass'));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'phpass'));
// Provider #2 (Google)
$plain = 'users-password';
$hash = 'EPKgfALpS9Tvgr/y1ki7ubY4AEGJeWL3teakrnmOacN4XGiyD00lkzEHgqCQ71wGxoi/zb7Y9a4orOtvMV3/Jw==';
$salt = '56dFqW+kswqktw==';
$saltSeparator = 'Bw==';
$signerKey = 'XyEKE9RcTDeLEsL/RjwPDBv/RqDl8fb3gpYEOQaPihbxf1ZAtSOHCjuAAa7Q3oHpCYhXSN9tizHgVOwn6krflQ==';
$options = [ 'salt' => $salt, 'saltSeparator' => $saltSeparator, 'signerKey' => $signerKey ];
$generatedHash = Auth::passwordHash($plain, 'scryptMod', $options);
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'scryptMod', $options));
$this->assertEquals(true, Auth::passwordVerify($plain, $hash, 'scryptMod', $options));
$this->assertEquals(false, Auth::passwordVerify('wrongPassword', $hash, 'scryptMod', $options));
}
public function testUnknownAlgo()
{
$this->expectExceptionMessage('Hashing algorithm \'md8\' is not supported.');
// Bcrypt - Cost 5
$plain = 'whatIsMd8?!?';
$generatedHash = Auth::passwordHash($plain, 'md8');
$this->assertEquals(true, Auth::passwordVerify($plain, $generatedHash, 'md8'));
}
public function testPasswordGenerator(): void
@ -64,6 +189,14 @@ class AuthTest extends TestCase
$this->assertEquals(\mb_strlen(Auth::tokenGenerator(5)), 10);
}
public function testCodeGenerator(): void
{
$this->assertEquals(6, \strlen(Auth::codeGenerator()));
$this->assertEquals(\mb_strlen(Auth::codeGenerator(256)), 256);
$this->assertEquals(\mb_strlen(Auth::codeGenerator(10)), 10);
$this->assertTrue(is_numeric(Auth::codeGenerator(5)));
}
public function testSessionVerify(): void
{
$secret = 'secret1';