From 5cefca1c22c708830258e820d7a8804b9dfd26cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 1 Mar 2024 16:22:51 +0000 Subject: [PATCH] Fix MFA flows and docs --- app/config/collections.php | 13 +- app/config/errors.php | 5 + app/controllers/api/account.php | 126 +++++++++++++----- app/controllers/api/users.php | 10 +- ...ticator.md => create-mfa-authenticator.md} | 0 .../account/create-mfa-challenge.md | 1 + .../account/create-mfa-recovery-codes.md | 1 + ...ete-mfa.md => delete-mfa-authenticator.md} | 0 .../{list-factors.md => list-mfa-factors.md} | 0 docs/references/account/update-challenge.md | 1 - ...ticator.md => update-mfa-authenticator.md} | 0 .../account/update-mfa-challenge.md | 1 + ...ete-mfa.md => delete-mfa-authenticator.md} | 0 .../{list-factors.md => list-mfa-factors.md} | 0 src/Appwrite/Auth/MFA/Type.php | 5 +- src/Appwrite/Extend/Exception.php | 1 + src/Appwrite/Utopia/Response.php | 3 + .../Utopia/Response/Model/Account.php | 1 + .../Response/Model/MFARecoveryCodes.php | 42 ++++++ .../Utopia/Response/Model/MFAType.php | 7 - 20 files changed, 169 insertions(+), 48 deletions(-) rename docs/references/account/{add-authenticator.md => create-mfa-authenticator.md} (100%) create mode 100644 docs/references/account/create-mfa-challenge.md create mode 100644 docs/references/account/create-mfa-recovery-codes.md rename docs/references/account/{delete-mfa.md => delete-mfa-authenticator.md} (100%) rename docs/references/account/{list-factors.md => list-mfa-factors.md} (100%) delete mode 100644 docs/references/account/update-challenge.md rename docs/references/account/{verify-authenticator.md => update-mfa-authenticator.md} (100%) create mode 100644 docs/references/account/update-mfa-challenge.md rename docs/references/users/{delete-mfa.md => delete-mfa-authenticator.md} (100%) rename docs/references/users/{list-factors.md => list-mfa-factors.md} (100%) create mode 100644 src/Appwrite/Utopia/Response/Model/MFARecoveryCodes.php diff --git a/app/config/collections.php b/app/config/collections.php index 97c4b31e3..e27105974 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -278,6 +278,17 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('mfaRecoveryCodes'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 256, + 'signed' => true, + 'required' => false, + 'default' => [], + 'array' => true, + 'filters' => [], + ], [ '$id' => ID::custom('authenticators'), 'type' => Database::VAR_STRING, @@ -365,7 +376,7 @@ $commonCollections = [ 'default' => null, 'array' => false, 'filters' => ['datetime'], - ] + ], ], 'indexes' => [ [ diff --git a/app/config/errors.php b/app/config/errors.php index 7a23b60e7..119f7a38c 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -232,6 +232,11 @@ 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_PHONE_NOT_FOUND => [ 'name' => Exception::USER_PHONE_NOT_FOUND, 'description' => 'The current user does not have a phone number associated with their account.', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index e7bc3c348..accaae012 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -3543,8 +3543,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) @@ -3565,7 +3565,7 @@ App::get('/v1/account/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') @@ -3575,8 +3575,8 @@ 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) @@ -3599,8 +3599,6 @@ App::post('/v1/account/mfa/:type') $otp->setLabel($user->getAttribute('email')); $otp->setIssuer($project->getAttribute('name')); - $backups = Type::generateBackupCodes(); - $authenticator = TOTP::getAuthenticatorFromUser($user); if ($authenticator) { @@ -3618,7 +3616,6 @@ App::post('/v1/account/mfa/:type') 'verified' => false, 'data' => [ 'secret' => $otp->getSecret(), - 'backups' => $backups ], '$permissions' => [ Permission::read(Role::user($user->getId())), @@ -3628,7 +3625,6 @@ App::post('/v1/account/mfa/:type') ]); $model = new Document([ - 'backups' => $backups, 'secret' => $otp->getSecret(), 'uri' => $otp->getProvisioningUri() ]); @@ -3641,7 +3637,7 @@ App::post('/v1/account/mfa/:type') $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') @@ -3651,8 +3647,8 @@ 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) @@ -3704,7 +3700,50 @@ 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_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') + ->inject('response') + ->inject('user') + ->inject('project') + ->inject('dbForProject') + ->inject('queueForEvents') + ->action(function (Response $response, Document $user, Document $project, 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::delete('/v1/account/mfa/authenticators/:type') ->desc('Delete Authenticator') ->groups(['api', 'account']) ->label('event', 'users.[userId].delete.mfa') @@ -3714,8 +3753,8 @@ 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) @@ -3736,8 +3775,22 @@ App::delete('/v1/account/mfa/:type') throw new Exception(Exception::GENERAL_UNKNOWN, 'Authenticator not found.'); } + $recoveryCodeVerify = function (Document $user, string $otp) use ($dbForProject) { + $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; + }; + $success = (match ($type) { Type::TOTP => Challenge\TOTP::verify($user, $otp), + Type::RECOVERY_CODE => $recoveryCodeVerify($user, $otp), default => false }); @@ -3763,14 +3816,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([Type::EMAIL, Type::PHONE, Type::TOTP]), 'Factor used for verification. Must be one of following: `' . Type::EMAIL . '`, `' . Type::PHONE . '`, `' . Type::TOTP . '`.') + ->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') @@ -3951,8 +4004,8 @@ App::put('/v1/account/mfa/challenge') ->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) @@ -3974,25 +4027,34 @@ App::put('/v1/account/mfa/challenge') $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; + }; + $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 && $type === Type::TOTP) { - $authenticator = TOTP::getAuthenticatorFromUser($user); - $data = $authenticator?->getAttribute('data', []); - if (in_array($otp, $data['backups'] ?? [])) { - $success = true; - $backups = array_diff($data['backups'], [$otp]); - $authenticator->setAttribute('data', array_merge($data, ['backups' => $backups])); - $dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator); - $dbForProject->purgeCachedDocument('users', $user->getId()); - } - } - if (!$success) { throw new Exception(Exception::USER_INVALID_TOKEN); } diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 9cac7f5d1..48750d0f3 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1565,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) @@ -1591,7 +1591,7 @@ App::get('/v1/users/:userId/mfa/factors') $response->dynamic($factors, Response::MODEL_MFA_FACTORS); }); -App::delete('/v1/users/:userId/mfa/:type') +App::delete('/v1/users/:userId/mfa/authenticators/:type') ->desc('Delete Authenticator') ->groups(['api', 'users']) ->label('event', 'users.[userId].delete.mfa') @@ -1602,8 +1602,8 @@ 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) diff --git a/docs/references/account/add-authenticator.md b/docs/references/account/create-mfa-authenticator.md similarity index 100% rename from docs/references/account/add-authenticator.md rename to docs/references/account/create-mfa-authenticator.md diff --git a/docs/references/account/create-mfa-challenge.md b/docs/references/account/create-mfa-challenge.md new file mode 100644 index 000000000..b1f1165ed --- /dev/null +++ b/docs/references/account/create-mfa-challenge.md @@ -0,0 +1 @@ +Begin the process of MFA verification after sign-in. Finish the flow with [updateMfaChallenge](/docs/references/cloud/client-web/account#updateMfaChallenge) method. \ No newline at end of file diff --git a/docs/references/account/create-mfa-recovery-codes.md b/docs/references/account/create-mfa-recovery-codes.md new file mode 100644 index 000000000..6d58ac849 --- /dev/null +++ b/docs/references/account/create-mfa-recovery-codes.md @@ -0,0 +1 @@ +Generate recovery codes as backup for MFA flow. It's recommended to generate and show then immediately after user successfully adds their authehticator. Recovery codes can be used as a MFA verification type in [createMfaChallenge](/docs/references/cloud/client-web/account#createMfaChallenge) method. \ No newline at end of file diff --git a/docs/references/account/delete-mfa.md b/docs/references/account/delete-mfa-authenticator.md similarity index 100% rename from docs/references/account/delete-mfa.md rename to docs/references/account/delete-mfa-authenticator.md diff --git a/docs/references/account/list-factors.md b/docs/references/account/list-mfa-factors.md similarity index 100% rename from docs/references/account/list-factors.md rename to docs/references/account/list-mfa-factors.md diff --git a/docs/references/account/update-challenge.md b/docs/references/account/update-challenge.md deleted file mode 100644 index 40cac99ce..000000000 --- a/docs/references/account/update-challenge.md +++ /dev/null @@ -1 +0,0 @@ -Complete the MFA challenge by providing the one-time password. \ No newline at end of file diff --git a/docs/references/account/verify-authenticator.md b/docs/references/account/update-mfa-authenticator.md similarity index 100% rename from docs/references/account/verify-authenticator.md rename to docs/references/account/update-mfa-authenticator.md diff --git a/docs/references/account/update-mfa-challenge.md b/docs/references/account/update-mfa-challenge.md new file mode 100644 index 000000000..5f3cc3c27 --- /dev/null +++ b/docs/references/account/update-mfa-challenge.md @@ -0,0 +1 @@ +Complete the MFA challenge by providing the one-time password. Finish the process of MFA verification by providing the one-time password. To begin the flow, use [createMfaChallenge](/docs/references/cloud/client-web/account#createMfaChallenge) method. \ No newline at end of file diff --git a/docs/references/users/delete-mfa.md b/docs/references/users/delete-mfa-authenticator.md similarity index 100% rename from docs/references/users/delete-mfa.md rename to docs/references/users/delete-mfa-authenticator.md diff --git a/docs/references/users/list-factors.md b/docs/references/users/list-mfa-factors.md similarity index 100% rename from docs/references/users/list-factors.md rename to docs/references/users/list-mfa-factors.md diff --git a/src/Appwrite/Auth/MFA/Type.php b/src/Appwrite/Auth/MFA/Type.php index f676bd019..3516ec378 100644 --- a/src/Appwrite/Auth/MFA/Type.php +++ b/src/Appwrite/Auth/MFA/Type.php @@ -12,6 +12,7 @@ abstract class Type public const TOTP = 'totp'; public const EMAIL = 'email'; public const PHONE = 'phone'; + public const RECOVERY_CODE = 'recoveryCode'; public function setLabel(string $label): self { @@ -47,12 +48,12 @@ abstract class Type return $this->instance->getProvisioningUri(); } - public static function generateBackupCodes(int $length = 6, int $total = 6): array + public static function generateBackupCodes(int $length = 10, int $total = 6): array { $backups = []; for ($i = 0; $i < $total; $i++) { - $backups[] = Auth::codeGenerator($length); + $backups[] = Auth::tokenGenerator($length); } return $backups; diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index fc56d213d..b39af2232 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -89,6 +89,7 @@ class Exception extends \Exception public const USER_MISSING_ID = 'user_missing_id'; public const USER_MORE_FACTORS_REQUIRED = 'user_more_factors_required'; public const USER_INVALID_CHALLENGE = 'user_invalid_challenge'; + public const USER_RECOVERY_CODES_ALREADY_EXISTS = 'user_recovery_codes_already_exists'; public const USER_OAUTH2_BAD_REQUEST = 'user_oauth2_bad_request'; public const USER_OAUTH2_UNAUTHORIZED = 'user_oauth2_unauthorized'; public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error'; diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 8bd8963d3..77e86212f 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -82,6 +82,7 @@ use Appwrite\Utopia\Response\Model\MetricBreakdown; use Appwrite\Utopia\Response\Model\Provider; use Appwrite\Utopia\Response\Model\Message; use Appwrite\Utopia\Response\Model\MFAFactors; +use Appwrite\Utopia\Response\Model\MFARecoveryCodes; use Appwrite\Utopia\Response\Model\MFAType; use Appwrite\Utopia\Response\Model\Subscriber; use Appwrite\Utopia\Response\Model\Topic; @@ -174,6 +175,7 @@ class Response extends SwooleResponse public const MODEL_MFA_FACTORS = 'mfaFactors'; public const MODEL_MFA_OTP = 'mfaTotp'; public const MODEL_MFA_CHALLENGE = 'mfaChallenge'; + public const MODEL_MFA_RECOVERY_CODES = 'mfaRecoveryCodes'; // Users password algos public const MODEL_ALGO_MD5 = 'algoMd5'; @@ -443,6 +445,7 @@ class Response extends SwooleResponse ->setModel(new TemplateEmail()) ->setModel(new ConsoleVariables()) ->setModel(new MFAChallenge()) + ->setModel(new MFARecoveryCodes()) ->setModel(new MFAType()) ->setModel(new MFAFactors()) ->setModel(new Provider()) diff --git a/src/Appwrite/Utopia/Response/Model/Account.php b/src/Appwrite/Utopia/Response/Model/Account.php index bfcd7ab7d..07fd4e92a 100644 --- a/src/Appwrite/Utopia/Response/Model/Account.php +++ b/src/Appwrite/Utopia/Response/Model/Account.php @@ -13,6 +13,7 @@ class Account extends User $this ->removeRule('password') ->removeRule('hash') + ->removeRule('mfaRecoveryCodes') ->removeRule('hashOptions'); } diff --git a/src/Appwrite/Utopia/Response/Model/MFARecoveryCodes.php b/src/Appwrite/Utopia/Response/Model/MFARecoveryCodes.php new file mode 100644 index 000000000..e190238c3 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/MFARecoveryCodes.php @@ -0,0 +1,42 @@ +addRule('recoveryCodes', [ + 'type' => self::TYPE_STRING, + 'description' => 'Recovery codes.', + 'array' => true, + 'default' => [], + 'example' => ['a3kf0-s0cl2', 's0co1-as98s'] + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'MFA Recovery Codes'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_MFA_RECOVERY_CODES; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/MFAType.php b/src/Appwrite/Utopia/Response/Model/MFAType.php index 8229e2365..5f4a27279 100644 --- a/src/Appwrite/Utopia/Response/Model/MFAType.php +++ b/src/Appwrite/Utopia/Response/Model/MFAType.php @@ -10,13 +10,6 @@ class MFAType extends Model public function __construct() { $this - ->addRule('backups', [ - 'type' => self::TYPE_STRING, - 'description' => 'Backup codes.', - 'array' => true, - 'default' => [], - 'example' => true - ]) ->addRule('secret', [ 'type' => self::TYPE_STRING, 'description' => 'Secret token used for TOTP factor.',