1
0
Fork 0
mirror of synced 2024-06-14 08:44:49 +12:00

Merge pull request #7696 from appwrite/feat-mfa-collection

feat: mfa collection restructure
This commit is contained in:
Torsten Dittmann 2024-03-04 10:18:49 +01:00 committed by GitHub
commit ad39c15d99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
371 changed files with 4042 additions and 296 deletions

View file

@ -17,7 +17,6 @@ tasks:
ports:
- port: 8080
onOpen: open-preview
visibility: public
vscode:

View file

@ -279,48 +279,26 @@ $commonCollections = [
'filters' => [],
],
[
'$id' => ID::custom('totp'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('totpVerification'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('totpSecret'),
'$id' => ID::custom('mfaRecoveryCodes'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
'default' => [],
'array' => true,
'filters' => ['encrypt'],
],
[
'$id' => ID::custom('totpBackup'),
'$id' => ID::custom('authenticators'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 6,
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
'array' => false,
'filters' => ['subQueryAuthenticators'],
],
[
'$id' => ID::custom('sessions'),
@ -398,7 +376,7 @@ $commonCollections = [
'default' => null,
'array' => false,
'filters' => ['datetime'],
]
],
],
'indexes' => [
[
@ -568,6 +546,78 @@ $commonCollections = [
],
],
'authenticators' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('authenticators'),
'name' => 'Authenticators',
'attributes' => [
[
'$id' => ID::custom('userInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('verified'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => false,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('data'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json', 'encrypt'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_userInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
]
],
],
'challenges' => [
'$collection' => ID::custom(Database::METADATA),
'$id' => ID::custom('challenges'),
@ -596,7 +646,7 @@ $commonCollections = [
'filters' => [],
],
[
'$id' => ID::custom('provider'),
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
@ -929,6 +979,17 @@ $commonCollections = [
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('mfaUpdatedAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
],
'indexes' => [
[

View file

@ -232,6 +232,26 @@ return [
'description' => 'A user with the same phone number already exists in the current project.',
'code' => 409,
],
Exception::USER_RECOVERY_CODES_ALREADY_EXISTS => [
'name' => Exception::USER_RECOVERY_CODES_ALREADY_EXISTS,
'description' => 'The current user already generated recovery codes and they can only be read once for security reasons.',
'code' => 409,
],
Exception::USER_AUTHENTICATOR_NOT_FOUND => [
'name' => Exception::USER_AUTHENTICATOR_NOT_FOUND,
'description' => 'Authenticator could not be found on the current user.',
'code' => 404,
],
Exception::USER_RECOVERY_CODES_NOT_FOUND => [
'name' => Exception::USER_RECOVERY_CODES_NOT_FOUND,
'description' => 'Recovery codes could not be found on the current user.',
'code' => 404,
],
Exception::USER_AUTHENTICATOR_ALREADY_VERIFIED => [
'name' => Exception::USER_AUTHENTICATOR_ALREADY_VERIFIED,
'description' => 'This authenticator is already verified on the current user.',
'code' => 409,
],
Exception::USER_PHONE_NOT_FOUND => [
'name' => Exception::USER_PHONE_NOT_FOUND,
'description' => 'The current user does not have a phone number associated with their account.',
@ -247,6 +267,11 @@ return [
'description' => 'More factors are required to complete the sign in process.',
'code' => 401,
],
Exception::USER_CHALLENGE_REQUIRED => [
'name' => Exception::USER_CHALLENGE_REQUIRED,
'description' => 'A recently succeessful challenge is required to complete this action. A challenge is considered recent for 5 minutes.',
'code' => 401,
],
Exception::USER_OAUTH2_BAD_REQUEST => [
'name' => Exception::USER_OAUTH2_BAD_REQUEST,
'description' => 'OAuth2 provider rejected the bad request.',

View file

@ -185,7 +185,7 @@ return [
[
'key' => 'web',
'name' => 'Console',
'version' => '0.6.0-rc.14',
'version' => '0.6.0-rc.15',
'url' => 'https://github.com/appwrite/sdk-for-console',
'package' => '',
'enabled' => true,

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

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

@ -1 +1 @@
Subproject commit 8391ef917780be26ccc8563024793994008e40ab
Subproject commit 4769c5018979b3f00393ce3015ff8bf69d9c1657

View file

@ -3,8 +3,8 @@
use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Challenge;
use Appwrite\Auth\MFA\Provider;
use Appwrite\Auth\MFA\Provider\TOTP;
use Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\OAuth2\Exception as OAuth2Exception;
use Appwrite\Auth\Validator\Password;
use Appwrite\Auth\Validator\Phone;
@ -151,11 +151,11 @@ App::post('/v1/account')
'reset' => false,
'name' => $name,
'mfa' => false,
'totp' => false,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'authenticators' => null,
'search' => implode(' ', [$userId, $email, $name]),
'accessedAt' => DateTime::now(),
]);
@ -766,11 +766,11 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'reset' => false,
'name' => $name,
'mfa' => false,
'totp' => false,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'authenticators' => null,
'search' => implode(' ', [$userId, $email, $name]),
'accessedAt' => DateTime::now(),
]);
@ -1152,11 +1152,11 @@ App::post('/v1/account/tokens/magic-url')
'registration' => DateTime::now(),
'reset' => false,
'mfa' => false,
'totp' => false,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'authenticators' => null,
'search' => implode(' ', [$userId, $email]),
'accessedAt' => DateTime::now(),
]);
@ -1565,14 +1565,14 @@ $createSession = function (string $userId, string $secret, Request $request, Res
$record = $geodb->get($request->getIP());
$sessionSecret = Auth::tokenGenerator(Auth::TOKEN_LENGTH_SESSION);
$factor = match ($verifiedToken->getAttribute('type')) {
$factor = (match ($verifiedToken->getAttribute('type')) {
Auth::TOKEN_TYPE_MAGIC_URL,
Auth::TOKEN_TYPE_OAUTH2,
Auth::TOKEN_TYPE_EMAIL => 'email',
Auth::TOKEN_TYPE_PHONE => 'phone',
Auth::TOKEN_TYPE_GENERIC => 'token',
default => throw new Exception(Exception::USER_INVALID_TOKEN)
};
});
$session = new Document(array_merge(
[
@ -1971,11 +1971,11 @@ App::post('/v1/account/sessions/anonymous')
'reset' => false,
'name' => null,
'mfa' => false,
'totp' => false,
'prefs' => new \stdClass(),
'sessions' => null,
'tokens' => null,
'memberships' => null,
'authenticators' => null,
'search' => $userId,
'accessedAt' => DateTime::now(),
]);
@ -2154,6 +2154,10 @@ App::get('/v1/account/sessions')
->inject('project')
->action(function (Response $response, Document $user, Locale $locale, Document $project) {
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$sessions = $user->getAttribute('sessions', []);
$current = Auth::sessionVerify($sessions, Auth::$secret);
@ -2162,6 +2166,8 @@ App::get('/v1/account/sessions')
$session->setAttribute('countryName', $countryName);
$session->setAttribute('current', ($current == $session->getId()) ? true : false);
$session->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $session->getAttribute('secret', '') : '');
$sessions[$key] = $session;
}
@ -2256,6 +2262,10 @@ App::get('/v1/account/sessions/:sessionId')
->inject('project')
->action(function (?string $sessionId, Response $response, Document $user, Locale $locale, Document $project) {
$roles = Authorization::getRoles();
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
$isAppUser = Auth::isAppUser($roles);
$sessions = $user->getAttribute('sessions', []);
$sessionId = ($sessionId === 'current')
? Auth::sessionVerify($user->getAttribute('sessions'), Auth::$secret)
@ -2268,6 +2278,7 @@ App::get('/v1/account/sessions/:sessionId')
$session
->setAttribute('current', ($session->getAttribute('secret') == Auth::hash(Auth::$secret)))
->setAttribute('countryName', $countryName)
->setAttribute('secret', ($isPrivilegedUser || $isAppUser) ? $session->getAttribute('secret', '') : '')
;
return $response->dynamic($session, Response::MODEL_SESSION);
@ -2630,7 +2641,7 @@ App::patch('/v1/account/status')
App::delete('/v1/account/sessions/:sessionId')
->desc('Delete session')
->groups(['api', 'account'])
->groups(['api', 'account', 'mfa'])
->label('scope', 'account')
->label('event', 'users.[userId].sessions.[sessionId].delete')
->label('audits.event', 'session.delete')
@ -3543,8 +3554,8 @@ App::get('/v1/account/mfa/factors')
->label('scope', 'account')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'listFactors')
->label('sdk.description', '/docs/references/account/list-factors.md')
->label('sdk.method', 'listMfaFactors')
->label('sdk.description', '/docs/references/account/list-mfa-factors.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_FACTORS)
@ -3554,16 +3565,18 @@ App::get('/v1/account/mfa/factors')
->inject('user')
->action(function (Response $response, Document $user) {
$providers = new Document([
'totp' => $user->getAttribute('totp', false) && $user->getAttribute('totpVerification', false),
'email' => $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false),
'phone' => $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false)
$totp = TOTP::getAuthenticatorFromUser($user);
$factors = new Document([
Type::TOTP => $totp !== null && $totp->getAttribute('verified', false),
Type::EMAIL => $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false),
Type::PHONE => $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false)
]);
$response->dynamic($providers, Response::MODEL_MFA_FACTORS);
$response->dynamic($factors, Response::MODEL_MFA_FACTORS);
});
App::post('/v1/account/mfa/:type')
App::post('/v1/account/mfa/authenticators/:type')
->desc('Add Authenticator')
->groups(['api', 'account'])
->label('event', 'users.[userId].update.mfa')
@ -3573,14 +3586,14 @@ App::post('/v1/account/mfa/:type')
->label('audits.userId', '{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'addAuthenticator')
->label('sdk.description', '/docs/references/account/add-authenticator.md')
->label('sdk.method', 'createMfaAuthenticator')
->label('sdk.description', '/docs/references/account/create-mfa-authenticator.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_TYPE)
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->param('type', null, new WhiteList(['totp']), 'Type of authenticator.')
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator. Must be `' . Type::TOTP . '`')
->inject('requestTimestamp')
->inject('response')
->inject('project')
@ -3589,40 +3602,53 @@ App::post('/v1/account/mfa/:type')
->inject('queueForEvents')
->action(function (string $type, ?\DateTime $requestTimestamp, Response $response, Document $project, Document $user, Database $dbForProject, Event $queueForEvents) {
$otp = match ($type) {
'totp' => new TOTP(),
default => throw new Exception(Exception::GENERAL_UNKNOWN, 'Unknown type.')
};
$otp = (match ($type) {
Type::TOTP => new TOTP(),
default => throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Unknown type.') // Ideally never happens if param validator stays always in sync
});
$otp->setLabel($user->getAttribute('email'));
$otp->setIssuer($project->getAttribute('name'));
$backups = Provider::generateBackupCodes();
$authenticator = TOTP::getAuthenticatorFromUser($user);
if ($user->getAttribute('totp') && $user->getAttribute('totpVerification')) {
throw new Exception(Exception::GENERAL_UNKNOWN, 'TOTP already exists on this account.');
}
if ($authenticator) {
if ($authenticator->getAttribute('verified')) {
throw new Exception(Exception::USER_AUTHENTICATOR_ALREADY_VERIFIED);
}
$dbForProject->deleteDocument('authenticators', $authenticator->getId());
}
$user
->setAttribute('totp', true)
->setAttribute('totpVerification', false)
->setAttribute('totpBackup', $backups)
->setAttribute('totpSecret', $otp->getSecret());
$authenticator = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Type::TOTP,
'verified' => false,
'data' => [
'secret' => $otp->getSecret(),
],
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]
]);
$model = new Document();
$model
->setAttribute('backups', $backups)
->setAttribute('secret', $otp->getSecret())
->setAttribute('uri', $otp->getProvisioningUri());
$model = new Document([
'secret' => $otp->getSecret(),
'uri' => $otp->getProvisioningUri()
]);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$authenticator = $dbForProject->createDocument('authenticators', $authenticator);
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents->setParam('userId', $user->getId());
$response->dynamic($model, Response::MODEL_MFA_TYPE);
});
App::put('/v1/account/mfa/:type')
App::put('/v1/account/mfa/authenticators/:type')
->desc('Verify Authenticator')
->groups(['api', 'account'])
->label('event', 'users.[userId].update.mfa')
@ -3632,41 +3658,48 @@ App::put('/v1/account/mfa/:type')
->label('audits.userId', '{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'verifyAuthenticator')
->label('sdk.description', '/docs/references/account/verify-authenticator.md')
->label('sdk.method', 'updateMfaAuthenticator')
->label('sdk.description', '/docs/references/account/update-mfa-authenticator.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.offline.model', '/account')
->label('sdk.offline.key', 'current')
->param('type', null, new WhiteList(['totp']), 'Type of authenticator.')
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.')
->param('otp', '', new Text(256), 'Valid verification token.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('project')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $type, string $otp, ?\DateTime $requestTimestamp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
->action(function (string $type, string $otp, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents) {
$success = match ($type) {
'totp' => Challenge\TOTP::verify($user, $otp),
$authenticator = (match ($type) {
Type::TOTP => TOTP::getAuthenticatorFromUser($user),
default => null
});
if ($authenticator === null) {
throw new Exception(Exception::USER_AUTHENTICATOR_NOT_FOUND);
}
if ($authenticator->getAttribute('verified')) {
throw new Exception(Exception::USER_AUTHENTICATOR_ALREADY_VERIFIED);
}
$success = (match ($type) {
Type::TOTP => Challenge\TOTP::verify($user, $otp),
default => false
};
});
if (!$success) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
if (!$success) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
if (!$user->getAttribute('totp')) {
throw new Exception(Exception::GENERAL_UNKNOWN, 'Authenticator needs to be added first.');
} elseif ($user->getAttribute('totpVerification')) {
throw new Exception(Exception::GENERAL_UNKNOWN, 'Authenticator already verified on this account.');
}
$authenticator->setAttribute('verified', true);
$user->setAttribute('totpVerification', true);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator);
$dbForProject->purgeCachedDocument('users', $user->getId());
$authDuration = $project->getAttribute('auths', [])['duration'] ?? Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$sessionId = Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret, $authDuration);
@ -3678,7 +3711,120 @@ App::put('/v1/account/mfa/:type')
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::delete('/v1/account/mfa/:type')
App::post('/v1/account/mfa/recovery-codes')
->desc('Create MFA Recovery Codes')
->groups(['api', 'account'])
->label('event', 'users.[userId].update.mfa')
->label('scope', 'account')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createMfaRecoveryCodes')
->label('sdk.description', '/docs/references/account/create-mfa-recovery-codes.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_RECOVERY_CODES)
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
if (!empty($mfaRecoveryCodes)) {
throw new Exception(Exception::USER_RECOVERY_CODES_ALREADY_EXISTS);
}
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
$document = new Document([
'recoveryCodes' => $mfaRecoveryCodes
]);
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::patch('/v1/account/mfa/recovery-codes')
->desc('Regenerate MFA Recovery Codes')
->groups(['api', 'account', 'mfaProtected'])
->label('event', 'users.[userId].update.mfa')
->label('scope', 'account')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateMfaRecoveryCodes')
->label('sdk.description', '/docs/references/account/update-mfa-recovery-codes.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_RECOVERY_CODES)
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->inject('dbForProject')
->inject('response')
->inject('user')
->inject('queueForEvents')
->action(function (Database $dbForProject, Response $response, Document $user, Event $queueForEvents) {
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
if (empty($mfaRecoveryCodes)) {
throw new Exception(Exception::USER_RECOVERY_CODES_NOT_FOUND);
}
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
$document = new Document([
'recoveryCodes' => $mfaRecoveryCodes
]);
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::get('/v1/account/mfa/recovery-codes')
->desc('Get MFA Recovery Codes')
->groups(['api', 'account', 'mfaProtected'])
->label('scope', 'account')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'getMfaRecoveryCodes')
->label('sdk.description', '/docs/references/account/get-mfa-recovery-codes.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_RECOVERY_CODES)
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->inject('response')
->inject('user')
->action(function (Response $response, Document $user) {
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
if (empty($mfaRecoveryCodes)) {
throw new Exception(Exception::USER_RECOVERY_CODES_NOT_FOUND);
}
$document = new Document([
'recoveryCodes' => $mfaRecoveryCodes
]);
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::delete('/v1/account/mfa/authenticators/:type')
->desc('Delete Authenticator')
->groups(['api', 'account'])
->label('event', 'users.[userId].delete.mfa')
@ -3688,40 +3834,50 @@ App::delete('/v1/account/mfa/:type')
->label('audits.userId', '{response.$id}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'deleteAuthenticator')
->label('sdk.description', '/docs/references/account/delete-mfa.md')
->label('sdk.method', 'deleteMfaAuthenticator')
->label('sdk.description', '/docs/references/account/delete-mfa-authenticator.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('type', null, new WhiteList(['totp']), 'Type of authenticator.')
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.')
->param('otp', '', new Text(256), 'Valid verification token.')
->inject('requestTimestamp')
->inject('response')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $type, string $otp, ?\DateTime $requestTimestamp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
->action(function (string $type, string $otp, Response $response, Document $user, Database $dbForProject, Event $queueForEvents) {
$success = match ($type) {
'totp' => Challenge\TOTP::verify($user, $otp),
$authenticator = (match ($type) {
Type::TOTP => TOTP::getAuthenticatorFromUser($user),
default => null
});
if (!$authenticator) {
throw new Exception(Exception::USER_AUTHENTICATOR_NOT_FOUND);
}
$success = (match ($type) {
Type::TOTP => Challenge\TOTP::verify($user, $otp),
default => false
};
});
if (!$success) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
if (!$success) {
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
if (in_array($otp, $mfaRecoveryCodes)) {
$mfaRecoveryCodes = array_diff($mfaRecoveryCodes, [$otp]);
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
if (!$user->getAttribute('totp')) {
throw new Exception(Exception::GENERAL_UNKNOWN, 'TOTP not added.');
}
$success = true;
}
}
$user
->setAttribute('totp', false)
->setAttribute('totpVerification', false)
->setAttribute('totpSecret', null)
->setAttribute('totpBackup', null);
if (!$success) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$dbForProject->deleteDocument('authenticators', $authenticator->getId());
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents->setParam('userId', $user->getId());
@ -3738,14 +3894,14 @@ App::post('/v1/account/mfa/challenge')
->label('audits.userId', '{response.userId}')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createChallenge')
->label('sdk.description', '/docs/references/account/create-challenge.md')
->label('sdk.method', 'createMfaChallenge')
->label('sdk.description', '/docs/references/account/create-mfa-challenge.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_CHALLENGE)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},token:{param-token}')
->param('factor', '', new WhiteList(['totp', 'phone', 'email']), 'Factor used for verification.')
->param('factor', '', new WhiteList([Type::EMAIL, Type::PHONE, Type::TOTP, Type::RECOVERY_CODE]), 'Factor used for verification. Must be one of following: `' . Type::EMAIL . '`, `' . Type::PHONE . '`, `' . Type::TOTP . '`, `' . Type::RECOVERY_CODE . '`.')
->inject('response')
->inject('dbForProject')
->inject('user')
@ -3762,7 +3918,7 @@ App::post('/v1/account/mfa/challenge')
$challenge = new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'provider' => $factor,
'type' => $factor,
'token' => Auth::tokenGenerator(),
'code' => $code,
'expire' => $expire,
@ -3776,7 +3932,7 @@ App::post('/v1/account/mfa/challenge')
$challenge = $dbForProject->createDocument('challenges', $challenge);
switch ($factor) {
case 'phone':
case Type::PHONE:
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@ -3814,7 +3970,7 @@ App::post('/v1/account/mfa/challenge')
->setRecipients([$user->getAttribute('phone')])
->setProviderType(MESSAGE_TYPE_SMS);
break;
case 'email':
case Type::EMAIL:
if (empty(App::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@ -3920,14 +4076,14 @@ App::put('/v1/account/mfa/challenge')
->desc('Create MFA Challenge (confirmation)')
->groups(['api', 'account', 'mfa'])
->label('scope', 'account')
->label('event', 'users.[userId].sessions.[tokenId].create')
->label('event', 'users.[userId].sessions.[sessionId].create')
->label('audits.event', 'challenges.update')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateChallenge')
->label('sdk.description', '/docs/references/account/update-challenge.md')
->label('sdk.method', 'updateMfaChallenge')
->label('sdk.description', '/docs/references/account/update-mfa-challenge.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
@ -3947,27 +4103,39 @@ App::put('/v1/account/mfa/challenge')
throw new Exception(Exception::USER_INVALID_TOKEN);
}
$provider = $challenge->getAttribute('provider');
$success = match ($provider) {
'totp' => Challenge\TOTP::challenge($challenge, $user, $otp),
'phone' => Challenge\Phone::challenge($challenge, $user, $otp),
'email' => Challenge\Email::challenge($challenge, $user, $otp),
default => false
$type = $challenge->getAttribute('type');
$recoveryCodeChallenge = function (Document $challenge, Document $user, string $otp) use ($dbForProject) {
if (
$challenge->isSet('type') &&
$challenge->getAttribute('type') === Type::RECOVERY_CODE
) {
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
if (in_array($otp, $mfaRecoveryCodes)) {
$mfaRecoveryCodes = array_diff($mfaRecoveryCodes, [$otp]);
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
return true;
}
return false;
}
return false;
};
if (!$success && $provider === 'totp') {
$backups = $user->getAttribute('totpBackup', []);
if (in_array($otp, $backups)) {
$success = true;
$backups = array_diff($backups, [$otp]);
$user->setAttribute('totpBackup', $backups);
$dbForProject->updateDocument('users', $user->getId(), $user);
}
}
$success = (match ($type) {
Type::TOTP => Challenge\TOTP::challenge($challenge, $user, $otp),
Type::PHONE => Challenge\Phone::challenge($challenge, $user, $otp),
Type::EMAIL => Challenge\Email::challenge($challenge, $user, $otp),
Type::RECOVERY_CODE => $recoveryCodeChallenge($challenge, $user, $otp),
default => false
});
if (!$success) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
if (!$success) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
$dbForProject->deleteDocument('challenges', $challengeId);
$dbForProject->purgeCachedDocument('users', $user->getId());
@ -3976,10 +4144,15 @@ App::put('/v1/account/mfa/challenge')
$sessionId = Auth::sessionVerify($user->getAttribute('sessions', []), Auth::$secret, $authDuration);
$session = $dbForProject->getDocument('sessions', $sessionId);
$dbForProject->updateDocument('sessions', $sessionId, $session->setAttribute('factors', $provider, Document::SET_TYPE_APPEND));
$session = $session
->setAttribute('factors', $type, Document::SET_TYPE_APPEND)
->setAttribute('mfaUpdatedAt', DateTime::now());
$dbForProject->updateDocument('sessions', $sessionId, $session);
$queueForEvents
->setParam('userId', $user->getId());
->setParam('userId', $user->getId())
->setParam('sessionId', $session->getId());
$response->dynamic($session, Response::MODEL_SESSION);
});

View file

@ -1,6 +1,7 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
@ -757,9 +758,9 @@ App::get('/v1/teams/:teamId/memberships')
$user = $dbForProject->getDocument('users', $membership->getAttribute('userId'));
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
$totpEnabled = $user->getAttribute('totp', false) && $user->getAttribute('totpVerification', false);
$totp = TOTP::getAuthenticatorFromUser($user);
$totpEnabled = $totp && $totp->getAttribute('verified', false);
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);
@ -820,7 +821,8 @@ App::get('/v1/teams/:teamId/memberships/:membershipId')
$mfa = $user->getAttribute('mfa', false);
if ($mfa) {
$totpEnabled = $user->getAttribute('totp', false) && $user->getAttribute('totpVerification', false);
$totp = TOTP::getAuthenticatorFromUser($user);
$totpEnabled = $totp && $totp->getAttribute('verified', false);
$emailEnabled = $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false);
$phoneEnabled = $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false);

View file

@ -1,7 +1,8 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Challenge;
use Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\Validator\Password;
use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
@ -1564,8 +1565,8 @@ App::get('/v1/users/:userId/mfa/factors')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'listFactors')
->label('sdk.description', '/docs/references/users/list-factors.md')
->label('sdk.method', 'listMfaFactors')
->label('sdk.description', '/docs/references/users/list-mfa-factors.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_FACTORS)
@ -1579,16 +1580,144 @@ App::get('/v1/users/:userId/mfa/factors')
throw new Exception(Exception::USER_NOT_FOUND);
}
$providers = new Document([
'totp' => $user->getAttribute('totp', false) && $user->getAttribute('totpVerification', false),
'email' => $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false),
'phone' => $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false)
$totp = TOTP::getAuthenticatorFromUser($user);
$factors = new Document([
Type::TOTP => $totp !== null && $totp->getAttribute('verified', false),
Type::EMAIL => $user->getAttribute('email', false) && $user->getAttribute('emailVerification', false),
Type::PHONE => $user->getAttribute('phone', false) && $user->getAttribute('phoneVerification', false)
]);
$response->dynamic($providers, Response::MODEL_MFA_FACTORS);
$response->dynamic($factors, Response::MODEL_MFA_FACTORS);
});
App::delete('/v1/users/:userId/mfa/:type')
App::get('/v1/users/:userId/mfa/recovery-codes')
->desc('Get MFA Recovery Codes')
->groups(['api', 'users'])
->label('scope', 'users.read')
->label('usage.metric', 'users.{scope}.requests.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getMfaRecoveryCodes')
->label('sdk.description', '/docs/references/users/get-mfa-recovery-codes.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_RECOVERY_CODES)
->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
if (empty($mfaRecoveryCodes)) {
throw new Exception(Exception::USER_RECOVERY_CODES_NOT_FOUND);
}
$document = new Document([
'recoveryCodes' => $mfaRecoveryCodes
]);
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::patch('/v1/users/:userId/mfa/recovery-codes')
->desc('Create MFA Recovery Codes')
->groups(['api', 'users'])
->label('event', 'users.[userId].create.mfa.recovery-codes')
->label('scope', 'users.write')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createMfaRecoveryCodes')
->label('sdk.description', '/docs/references/users/create-mfa-recovery-codes.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_RECOVERY_CODES)
->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
if (!empty($mfaRecoveryCodes)) {
throw new Exception(Exception::USER_RECOVERY_CODES_ALREADY_EXISTS);
}
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
$document = new Document([
'recoveryCodes' => $mfaRecoveryCodes
]);
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::put('/v1/users/:userId/mfa/recovery-codes')
->desc('Regenerate MFA Recovery Codes')
->groups(['api', 'users'])
->label('event', 'users.[userId].update.mfa.recovery-codes')
->label('scope', 'users.write')
->label('audits.event', 'user.update')
->label('audits.resource', 'user/{response.$id}')
->label('audits.userId', '{response.$id}')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateMfaRecoveryCodes')
->label('sdk.description', '/docs/references/users/update-mfa-recovery-codes.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_MFA_RECOVERY_CODES)
->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $userId, Response $response, Database $dbForProject, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$mfaRecoveryCodes = $user->getAttribute('mfaRecoveryCodes', []);
if (empty($mfaRecoveryCodes)) {
throw new Exception(Exception::USER_RECOVERY_CODES_NOT_FOUND);
}
$mfaRecoveryCodes = Type::generateBackupCodes();
$user->setAttribute('mfaRecoveryCodes', $mfaRecoveryCodes);
$dbForProject->updateDocument('users', $user->getId(), $user);
$queueForEvents->setParam('userId', $user->getId());
$document = new Document([
'recoveryCodes' => $mfaRecoveryCodes
]);
$response->dynamic($document, Response::MODEL_MFA_RECOVERY_CODES);
});
App::delete('/v1/users/:userId/mfa/authenticators/:type')
->desc('Delete Authenticator')
->groups(['api', 'users'])
->label('event', 'users.[userId].delete.mfa')
@ -1599,35 +1728,31 @@ App::delete('/v1/users/:userId/mfa/:type')
->label('usage.metric', 'users.{scope}.requests.update')
->label('sdk.auth', [APP_AUTH_TYPE_KEY])
->label('sdk.namespace', 'users')
->label('sdk.method', 'deleteAuthenticator')
->label('sdk.description', '/docs/references/users/delete-mfa.md')
->label('sdk.method', 'deleteMfaAuthenticator')
->label('sdk.description', '/docs/references/users/delete-mfa-authenticator.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('userId', '', new UID(), 'User ID.')
->param('type', null, new WhiteList(['totp']), 'Type of authenticator.')
->inject('requestTimestamp')
->param('type', null, new WhiteList([Type::TOTP]), 'Type of authenticator.')
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $userId, string $type, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents) {
->action(function (string $userId, string $type, Response $response, Database $dbForProject, Event $queueForEvents) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
if (!$user->getAttribute('totp')) {
throw new Exception(Exception::GENERAL_UNKNOWN, 'TOTP not added.');
$authenticator = TOTP::getAuthenticatorFromUser($user);
if ($authenticator === null) {
throw new Exception(Exception::USER_AUTHENTICATOR_NOT_FOUND);
}
$user
->setAttribute('totp', false)
->setAttribute('totpVerification', false)
->setAttribute('totpSecret', null)
->setAttribute('totpBackup', null);
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
$dbForProject->deleteDocument('authenticators', $authenticator->getId());
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents->setParam('userId', $user->getId());

View file

@ -1,6 +1,7 @@
<?php
use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Event\Audit;
use Appwrite\Event\Build;
use Appwrite\Event\Database as EventDatabase;
@ -280,9 +281,9 @@ App::init()
if ($mode !== APP_MODE_ADMIN) {
$mfaEnabled = $user->getAttribute('mfa', false);
$hasVerifiedAuthenticator = $user->getAttribute('totpVerification', false);
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
$hasVerifiedAuthenticator = TOTP::getAuthenticatorFromUser($user)?->getAttribute('verified') ?? false;
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
$minimumFactors = ($mfaEnabled && $hasMoreFactors) ? 2 : 1;

View file

@ -7,6 +7,26 @@ use Appwrite\Extend\Exception;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use MaxMind\Db\Reader;
use Utopia\Database\DateTime;
App::init()
->groups(['mfaProtected'])
->inject('session')
->action(function (Document $session) {
$isSessionFresh = false;
$lastUpdate = $session->getAttribute('mfaUpdatedAt');
if (!empty($lastUpdate)) {
$now = DateTime::now();
$maxAllowedDate = DateTime::addSeconds($lastUpdate, Auth::MFA_RECENT_DURATION); // Maximum date until session is considered safe before asking for another challenge
$isSessionFresh = DateTime::formatTz($maxAllowedDate) >= DateTime::formatTz($now);
}
if (!$isSessionFresh) {
throw new Exception(Exception::USER_CHALLENGE_REQUIRED);
}
});
App::init()
->groups(['auth'])

View file

@ -471,6 +471,20 @@ Database::addFilter(
}
);
Database::addFilter(
'subQueryAuthenticators',
function (mixed $value) {
return null;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn() => $database
->find('authenticators', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
Database::addFilter(
'subQueryMemberships',
function (mixed $value) {

View file

@ -0,0 +1,23 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
import io.appwrite.enums.Type;
Client client = new Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createMfaAuthenticator(
type.TOTP, // type
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -0,0 +1,23 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
import io.appwrite.enums.Factor;
Client client = new Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createMfaChallenge(
factor.EMAIL, // factor
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -0,0 +1,18 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
Client client = new Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.createMfaRecoveryCodes(new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
}));

View file

@ -0,0 +1,24 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
import io.appwrite.enums.Type;
Client client = new Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.deleteMfaAuthenticator(
type.TOTP, // type
"<OTP>", // otp
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -0,0 +1,18 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
Client client = new Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.getMfaRecoveryCodes(new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
}));

View file

@ -0,0 +1,18 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
Client client = new Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.listMfaFactors(new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
}));

View file

@ -0,0 +1,24 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
import io.appwrite.enums.Type;
Client client = new Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updateMfaAuthenticator(
type.TOTP, // type
"<OTP>", // otp
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -0,0 +1,23 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
Client client = new Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updateMfaChallenge(
"<CHALLENGE_ID>", // challengeId
"<OTP>", // otp
new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
})
);

View file

@ -0,0 +1,18 @@
import io.appwrite.Client;
import io.appwrite.coroutines.CoroutineCallback;
import io.appwrite.services.Account;
Client client = new Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2"); // Your project ID
Account account = new Account(client);
account.updateMfaRecoveryCodes(new CoroutineCallback<>((result, error) -> {
if (error != null) {
error.printStackTrace();
return;
}
Log.d("Appwrite", result.toString());
}));

View file

@ -0,0 +1,14 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Account
import io.appwrite.enums.Type
val client = Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
val account = Account(client)
val result = account.createMfaAuthenticator(
type = type.TOTP,
)

View file

@ -0,0 +1,14 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Account
import io.appwrite.enums.Factor
val client = Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
val account = Account(client)
val result = account.createMfaChallenge(
factor = factor.EMAIL,
)

View file

@ -0,0 +1,11 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Account
val client = Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
val account = Account(client)
val result = account.createMfaRecoveryCodes()

View file

@ -0,0 +1,15 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Account
import io.appwrite.enums.Type
val client = Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
val account = Account(client)
val result = account.deleteMfaAuthenticator(
type = type.TOTP,
otp = "<OTP>",
)

View file

@ -0,0 +1,11 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Account
val client = Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
val account = Account(client)
val result = account.getMfaRecoveryCodes()

View file

@ -0,0 +1,11 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Account
val client = Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
val account = Account(client)
val result = account.listMfaFactors()

View file

@ -0,0 +1,15 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Account
import io.appwrite.enums.Type
val client = Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
val account = Account(client)
val result = account.updateMfaAuthenticator(
type = type.TOTP,
otp = "<OTP>",
)

View file

@ -0,0 +1,14 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Account
val client = Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
val account = Account(client)
val result = account.updateMfaChallenge(
challengeId = "<CHALLENGE_ID>",
otp = "<OTP>",
)

View file

@ -0,0 +1,11 @@
import io.appwrite.Client
import io.appwrite.coroutines.CoroutineCallback
import io.appwrite.services.Account
val client = Client(context)
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
val account = Account(client)
val result = account.updateMfaRecoveryCodes()

View file

@ -0,0 +1,13 @@
import Appwrite
import AppwriteEnums
let client = Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
let account = Account(client)
let mfaType = try await account.createMfaAuthenticator(
type: .totp
)

View file

@ -0,0 +1,13 @@
import Appwrite
import AppwriteEnums
let client = Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
let account = Account(client)
let mfaChallenge = try await account.createMfaChallenge(
factor: .email
)

View file

@ -0,0 +1,10 @@
import Appwrite
let client = Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
let account = Account(client)
let mfaRecoveryCodes = try await account.createMfaRecoveryCodes()

View file

@ -0,0 +1,14 @@
import Appwrite
import AppwriteEnums
let client = Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
let account = Account(client)
let user = try await account.deleteMfaAuthenticator(
type: .totp,
otp: "<OTP>"
)

View file

@ -0,0 +1,10 @@
import Appwrite
let client = Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
let account = Account(client)
let mfaRecoveryCodes = try await account.getMfaRecoveryCodes()

View file

@ -0,0 +1,10 @@
import Appwrite
let client = Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
let account = Account(client)
let mfaFactors = try await account.listMfaFactors()

View file

@ -0,0 +1,14 @@
import Appwrite
import AppwriteEnums
let client = Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
let account = Account(client)
let user = try await account.updateMfaAuthenticator(
type: .totp,
otp: "<OTP>"
)

View file

@ -0,0 +1,13 @@
import Appwrite
let client = Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
let account = Account(client)
let result = try await account.updateMfaChallenge(
challengeId: "<CHALLENGE_ID>",
otp: "<OTP>"
)

View file

@ -0,0 +1,10 @@
import Appwrite
let client = Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Your API Endpoint
.setProject("5df5acd0d48c2") // Your project ID
let account = Account(client)
let mfaRecoveryCodes = try await account.updateMfaRecoveryCodes()

View file

@ -0,0 +1,11 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
Account account = Account(client);
MfaType result = await account.createMfaAuthenticator(
type: .totp,
);

View file

@ -0,0 +1,11 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
Account account = Account(client);
MfaChallenge result = await account.createMfaChallenge(
factor: .email,
);

View file

@ -0,0 +1,9 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
Account account = Account(client);
MfaRecoveryCodes result = await account.createMfaRecoveryCodes();

View file

@ -0,0 +1,12 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
Account account = Account(client);
await account.deleteMfaAuthenticator(
type: .totp,
otp: '<OTP>',
);

View file

@ -0,0 +1,9 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
Account account = Account(client);
MfaRecoveryCodes result = await account.getMfaRecoveryCodes();

View file

@ -0,0 +1,9 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
Account account = Account(client);
MfaFactors result = await account.listMfaFactors();

View file

@ -0,0 +1,12 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
Account account = Account(client);
User result = await account.updateMfaAuthenticator(
type: .totp,
otp: '<OTP>',
);

View file

@ -0,0 +1,12 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
Account account = Account(client);
result = await account.updateMfaChallenge(
challengeId: '<CHALLENGE_ID>',
otp: '<OTP>',
);

View file

@ -0,0 +1,9 @@
import 'package:appwrite/appwrite.dart';
Client client = Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
Account account = Account(client);
MfaRecoveryCodes result = await account.updateMfaRecoveryCodes();

View file

@ -27,5 +27,6 @@ mutation {
current
factors
secret
mfaUpdatedAt
}
}

View file

@ -30,5 +30,6 @@ mutation {
current
factors
secret
mfaUpdatedAt
}
}

View file

@ -0,0 +1,8 @@
mutation {
accountCreateMfaAuthenticator(
type: "totp"
) {
secret
uri
}
}

View file

@ -0,0 +1,10 @@
mutation {
accountCreateMfaChallenge(
factor: "email"
) {
_id
_createdAt
userId
expire
}
}

View file

@ -0,0 +1,5 @@
mutation {
accountCreateMfaRecoveryCodes {
recoveryCodes
}
}

View file

@ -30,5 +30,6 @@ mutation {
current
factors
secret
mfaUpdatedAt
}
}

View file

@ -21,7 +21,6 @@ mutation {
emailVerification
phoneVerification
mfa
totp
prefs {
data
}

View file

@ -0,0 +1,37 @@
mutation {
accountDeleteMfaAuthenticator(
type: "totp",
otp: "<OTP>"
) {
_id
_createdAt
_updatedAt
name
password
hash
hashOptions
registration
status
labels
passwordUpdate
email
phone
emailVerification
phoneVerification
mfa
prefs {
data
}
targets {
_id
_createdAt
_updatedAt
name
userId
providerId
providerType
identifier
}
accessedAt
}
}

View file

@ -0,0 +1,5 @@
query {
accountGetMfaRecoveryCodes {
recoveryCodes
}
}

View file

@ -29,5 +29,6 @@ query {
current
factors
secret
mfaUpdatedAt
}
}

View file

@ -16,7 +16,6 @@ query {
emailVerification
phoneVerification
mfa
totp
prefs {
data
}

View file

@ -0,0 +1,7 @@
query {
accountListMfaFactors {
totp
phone
email
}
}

View file

@ -29,6 +29,7 @@ query {
current
factors
secret
mfaUpdatedAt
}
}
}

View file

@ -19,7 +19,6 @@ mutation {
emailVerification
phoneVerification
mfa
totp
prefs {
data
}

View file

@ -18,7 +18,6 @@ mutation {
emailVerification
phoneVerification
mfa
totp
prefs {
data
}

View file

@ -30,5 +30,6 @@ mutation {
current
factors
secret
mfaUpdatedAt
}
}

View file

@ -0,0 +1,37 @@
mutation {
accountUpdateMfaAuthenticator(
type: "totp",
otp: "<OTP>"
) {
_id
_createdAt
_updatedAt
name
password
hash
hashOptions
registration
status
labels
passwordUpdate
email
phone
emailVerification
phoneVerification
mfa
prefs {
data
}
targets {
_id
_createdAt
_updatedAt
name
userId
providerId
providerType
identifier
}
accessedAt
}
}

View file

@ -0,0 +1,8 @@
mutation {
accountUpdateMfaChallenge(
challengeId: "<CHALLENGE_ID>",
otp: "<OTP>"
) {
status
}
}

View file

@ -0,0 +1,5 @@
mutation {
accountUpdateMfaRecoveryCodes {
recoveryCodes
}
}

View file

@ -18,7 +18,6 @@ mutation {
emailVerification
phoneVerification
mfa
totp
prefs {
data
}

View file

@ -19,7 +19,6 @@ mutation {
emailVerification
phoneVerification
mfa
totp
prefs {
data
}

View file

@ -30,5 +30,6 @@ mutation {
current
factors
secret
mfaUpdatedAt
}
}

View file

@ -19,7 +19,6 @@ mutation {
emailVerification
phoneVerification
mfa
totp
prefs {
data
}

View file

@ -18,7 +18,6 @@ mutation {
emailVerification
phoneVerification
mfa
totp
prefs {
data
}

View file

@ -29,5 +29,6 @@ mutation {
current
factors
secret
mfaUpdatedAt
}
}

View file

@ -16,7 +16,6 @@ mutation {
emailVerification
phoneVerification
mfa
totp
prefs {
data
}

View file

@ -0,0 +1,8 @@
POST /v1/account/mfa/authenticators/{type} HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
X-Appwrite-Project: 5df5acd0d48c2
X-Appwrite-Session:
X-Appwrite-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...

View file

@ -0,0 +1,9 @@
POST /v1/account/mfa/challenge HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
X-Appwrite-Project: 5df5acd0d48c2
{
"factor": "email"
}

View file

@ -0,0 +1,8 @@
POST /v1/account/mfa/recovery-codes HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
X-Appwrite-Project: 5df5acd0d48c2
X-Appwrite-Session:
X-Appwrite-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...

View file

@ -0,0 +1,11 @@
DELETE /v1/account/mfa/authenticators/{type} HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
X-Appwrite-Project: 5df5acd0d48c2
X-Appwrite-Session:
X-Appwrite-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...
{
"otp": "<OTP>"
}

View file

@ -0,0 +1,8 @@
GET /v1/account/mfa/recovery-codes HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
X-Appwrite-Project: 5df5acd0d48c2
X-Appwrite-Session:
X-Appwrite-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...

View file

@ -0,0 +1,8 @@
GET /v1/account/mfa/factors HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
X-Appwrite-Project: 5df5acd0d48c2
X-Appwrite-Session:
X-Appwrite-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...

View file

@ -0,0 +1,11 @@
PUT /v1/account/mfa/authenticators/{type} HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
X-Appwrite-Project: 5df5acd0d48c2
X-Appwrite-Session:
X-Appwrite-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...
{
"otp": "<OTP>"
}

View file

@ -0,0 +1,12 @@
PUT /v1/account/mfa/challenge HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
X-Appwrite-Project: 5df5acd0d48c2
X-Appwrite-Session:
X-Appwrite-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...
{
"challengeId": "<CHALLENGE_ID>",
"otp": "<OTP>"
}

View file

@ -0,0 +1,8 @@
PATCH /v1/account/mfa/recovery-codes HTTP/1.1
Host: cloud.appwrite.io
Content-Type: application/json
X-Appwrite-Response-Format: 1.5.0
X-Appwrite-Project: 5df5acd0d48c2
X-Appwrite-Session:
X-Appwrite-JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...

View file

@ -0,0 +1,13 @@
import { Client, Account, } from "appwrite";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
const account = new Account(client);
const result = await account.createMfaAuthenticator(
.Totp // type
);
console.log(response);

View file

@ -0,0 +1,13 @@
import { Client, Account, } from "appwrite";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
const account = new Account(client);
const result = await account.createMfaChallenge(
.Email // factor
);
console.log(response);

View file

@ -0,0 +1,11 @@
import { Client, Account } from "appwrite";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
const account = new Account(client);
const result = await account.createMfaRecoveryCodes();
console.log(response);

View file

@ -0,0 +1,14 @@
import { Client, Account, } from "appwrite";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
const account = new Account(client);
const result = await account.deleteMfaAuthenticator(
.Totp, // type
'<OTP>' // otp
);
console.log(response);

View file

@ -0,0 +1,11 @@
import { Client, Account } from "appwrite";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
const account = new Account(client);
const result = await account.getMfaRecoveryCodes();
console.log(response);

View file

@ -0,0 +1,11 @@
import { Client, Account } from "appwrite";
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('5df5acd0d48c2'); // Your project ID
const account = new Account(client);
const result = await account.listMfaFactors();
console.log(response);

Some files were not shown because too many files have changed in this diff Show more