Merge pull request #7696 from appwrite/feat-mfa-collection
feat: mfa collection restructure
This commit is contained in:
commit
ad39c15d99
|
@ -17,7 +17,6 @@ tasks:
|
|||
|
||||
ports:
|
||||
- port: 8080
|
||||
onOpen: open-preview
|
||||
visibility: public
|
||||
|
||||
vscode:
|
||||
|
|
|
@ -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' => [
|
||||
[
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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'])
|
||||
|
|
14
app/init.php
14
app/init.php
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
})
|
||||
);
|
||||
|
|
@ -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());
|
||||
})
|
||||
);
|
||||
|
|
@ -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());
|
||||
}));
|
|
@ -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());
|
||||
})
|
||||
);
|
||||
|
|
@ -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());
|
||||
}));
|
|
@ -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());
|
||||
}));
|
|
@ -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());
|
||||
})
|
||||
);
|
||||
|
|
@ -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());
|
||||
})
|
||||
);
|
||||
|
|
@ -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());
|
||||
}));
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
)
|
|
@ -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()
|
|
@ -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>",
|
||||
)
|
|
@ -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()
|
|
@ -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()
|
|
@ -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>",
|
||||
)
|
|
@ -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>",
|
||||
)
|
|
@ -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()
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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>"
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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>"
|
||||
)
|
||||
|
|
@ -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>"
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
);
|
|
@ -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,
|
||||
);
|
|
@ -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();
|
|
@ -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>',
|
||||
);
|
|
@ -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();
|
|
@ -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();
|
|
@ -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>',
|
||||
);
|
|
@ -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>',
|
||||
);
|
|
@ -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();
|
|
@ -27,5 +27,6 @@ mutation {
|
|||
current
|
||||
factors
|
||||
secret
|
||||
mfaUpdatedAt
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,5 +30,6 @@ mutation {
|
|||
current
|
||||
factors
|
||||
secret
|
||||
mfaUpdatedAt
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
mutation {
|
||||
accountCreateMfaAuthenticator(
|
||||
type: "totp"
|
||||
) {
|
||||
secret
|
||||
uri
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
mutation {
|
||||
accountCreateMfaChallenge(
|
||||
factor: "email"
|
||||
) {
|
||||
_id
|
||||
_createdAt
|
||||
userId
|
||||
expire
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
mutation {
|
||||
accountCreateMfaRecoveryCodes {
|
||||
recoveryCodes
|
||||
}
|
||||
}
|
|
@ -30,5 +30,6 @@ mutation {
|
|||
current
|
||||
factors
|
||||
secret
|
||||
mfaUpdatedAt
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ mutation {
|
|||
emailVerification
|
||||
phoneVerification
|
||||
mfa
|
||||
totp
|
||||
prefs {
|
||||
data
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
query {
|
||||
accountGetMfaRecoveryCodes {
|
||||
recoveryCodes
|
||||
}
|
||||
}
|
|
@ -29,5 +29,6 @@ query {
|
|||
current
|
||||
factors
|
||||
secret
|
||||
mfaUpdatedAt
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ query {
|
|||
emailVerification
|
||||
phoneVerification
|
||||
mfa
|
||||
totp
|
||||
prefs {
|
||||
data
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
query {
|
||||
accountListMfaFactors {
|
||||
totp
|
||||
phone
|
||||
email
|
||||
}
|
||||
}
|
|
@ -29,6 +29,7 @@ query {
|
|||
current
|
||||
factors
|
||||
secret
|
||||
mfaUpdatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ mutation {
|
|||
emailVerification
|
||||
phoneVerification
|
||||
mfa
|
||||
totp
|
||||
prefs {
|
||||
data
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ mutation {
|
|||
emailVerification
|
||||
phoneVerification
|
||||
mfa
|
||||
totp
|
||||
prefs {
|
||||
data
|
||||
}
|
||||
|
|
|
@ -30,5 +30,6 @@ mutation {
|
|||
current
|
||||
factors
|
||||
secret
|
||||
mfaUpdatedAt
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
mutation {
|
||||
accountUpdateMfaChallenge(
|
||||
challengeId: "<CHALLENGE_ID>",
|
||||
otp: "<OTP>"
|
||||
) {
|
||||
status
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
mutation {
|
||||
accountUpdateMfaRecoveryCodes {
|
||||
recoveryCodes
|
||||
}
|
||||
}
|
|
@ -18,7 +18,6 @@ mutation {
|
|||
emailVerification
|
||||
phoneVerification
|
||||
mfa
|
||||
totp
|
||||
prefs {
|
||||
data
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ mutation {
|
|||
emailVerification
|
||||
phoneVerification
|
||||
mfa
|
||||
totp
|
||||
prefs {
|
||||
data
|
||||
}
|
||||
|
|
|
@ -30,5 +30,6 @@ mutation {
|
|||
current
|
||||
factors
|
||||
secret
|
||||
mfaUpdatedAt
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ mutation {
|
|||
emailVerification
|
||||
phoneVerification
|
||||
mfa
|
||||
totp
|
||||
prefs {
|
||||
data
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ mutation {
|
|||
emailVerification
|
||||
phoneVerification
|
||||
mfa
|
||||
totp
|
||||
prefs {
|
||||
data
|
||||
}
|
||||
|
|
|
@ -29,5 +29,6 @@ mutation {
|
|||
current
|
||||
factors
|
||||
secret
|
||||
mfaUpdatedAt
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ mutation {
|
|||
emailVerification
|
||||
phoneVerification
|
||||
mfa
|
||||
totp
|
||||
prefs {
|
||||
data
|
||||
}
|
||||
|
|
|
@ -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...
|
||||
|
|
@ -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"
|
||||
}
|
|
@ -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...
|
||||
|
|
@ -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>"
|
||||
}
|
|
@ -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...
|
||||
|
|
@ -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...
|
||||
|
|
@ -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>"
|
||||
}
|
|
@ -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>"
|
||||
}
|
|
@ -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...
|
||||
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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
Loading…
Reference in a new issue