feat: mfa users endpoints
This commit is contained in:
parent
e7c40d1cb1
commit
057496dbaa
10 changed files with 147 additions and 17 deletions
|
@ -185,7 +185,7 @@ return [
|
||||||
[
|
[
|
||||||
'key' => 'web',
|
'key' => 'web',
|
||||||
'name' => 'Console',
|
'name' => 'Console',
|
||||||
'version' => '0.6.0-rc.5',
|
'version' => '0.6.0-rc.6',
|
||||||
'url' => 'https://github.com/appwrite/sdk-for-console',
|
'url' => 'https://github.com/appwrite/sdk-for-console',
|
||||||
'package' => '',
|
'package' => '',
|
||||||
'enabled' => true,
|
'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
|
@ -3228,7 +3228,7 @@ App::patch('/v1/account/mfa')
|
||||||
->desc('Update MFA')
|
->desc('Update MFA')
|
||||||
->groups(['api', 'account'])
|
->groups(['api', 'account'])
|
||||||
->label('event', 'users.[userId].update.mfa')
|
->label('event', 'users.[userId].update.mfa')
|
||||||
->label('scope', 'account')
|
->label('scope', 'accounts.write')
|
||||||
->label('audits.event', 'user.update')
|
->label('audits.event', 'user.update')
|
||||||
->label('audits.resource', 'user/{response.$id}')
|
->label('audits.resource', 'user/{response.$id}')
|
||||||
->label('audits.userId', '{response.$id}')
|
->label('audits.userId', '{response.$id}')
|
||||||
|
@ -3242,7 +3242,7 @@ App::patch('/v1/account/mfa')
|
||||||
->label('sdk.response.model', Response::MODEL_USER)
|
->label('sdk.response.model', Response::MODEL_USER)
|
||||||
->label('sdk.offline.model', '/account')
|
->label('sdk.offline.model', '/account')
|
||||||
->label('sdk.offline.key', 'current')
|
->label('sdk.offline.key', 'current')
|
||||||
->param('mfa', null, new Boolean(), 'User name. Max length: 128 chars.')
|
->param('mfa', null, new Boolean(), 'Enable or disable MFA.')
|
||||||
->inject('requestTimestamp')
|
->inject('requestTimestamp')
|
||||||
->inject('response')
|
->inject('response')
|
||||||
->inject('user')
|
->inject('user')
|
||||||
|
@ -3262,7 +3262,7 @@ App::patch('/v1/account/mfa')
|
||||||
App::get('/v1/account/mfa/providers')
|
App::get('/v1/account/mfa/providers')
|
||||||
->desc('List Providers')
|
->desc('List Providers')
|
||||||
->groups(['api', 'account', 'mfa'])
|
->groups(['api', 'account', 'mfa'])
|
||||||
->label('scope', 'account')
|
->label('scope', 'accounts.read')
|
||||||
->label('usage.metric', 'users.{scope}.requests.read')
|
->label('usage.metric', 'users.{scope}.requests.read')
|
||||||
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||||
->label('sdk.namespace', 'account')
|
->label('sdk.namespace', 'account')
|
||||||
|
@ -3290,7 +3290,7 @@ App::post('/v1/account/mfa/:provider')
|
||||||
->desc('Add Authenticator')
|
->desc('Add Authenticator')
|
||||||
->groups(['api', 'account', 'mfa'])
|
->groups(['api', 'account', 'mfa'])
|
||||||
->label('event', 'users.[userId].update.mfa')
|
->label('event', 'users.[userId].update.mfa')
|
||||||
->label('scope', 'account')
|
->label('scope', 'accounts.write')
|
||||||
->label('audits.event', 'user.update')
|
->label('audits.event', 'user.update')
|
||||||
->label('audits.resource', 'user/{response.$id}')
|
->label('audits.resource', 'user/{response.$id}')
|
||||||
->label('audits.userId', '{response.$id}')
|
->label('audits.userId', '{response.$id}')
|
||||||
|
@ -3350,7 +3350,7 @@ App::put('/v1/account/mfa/:provider')
|
||||||
->desc('Verify Authenticator')
|
->desc('Verify Authenticator')
|
||||||
->groups(['api', 'account', 'mfa'])
|
->groups(['api', 'account', 'mfa'])
|
||||||
->label('event', 'users.[userId].update.mfa')
|
->label('event', 'users.[userId].update.mfa')
|
||||||
->label('scope', 'account')
|
->label('scope', 'accounts.write')
|
||||||
->label('audits.event', 'user.update')
|
->label('audits.event', 'user.update')
|
||||||
->label('audits.resource', 'user/{response.$id}')
|
->label('audits.resource', 'user/{response.$id}')
|
||||||
->label('audits.userId', '{response.$id}')
|
->label('audits.userId', '{response.$id}')
|
||||||
|
@ -3405,9 +3405,9 @@ App::put('/v1/account/mfa/:provider')
|
||||||
|
|
||||||
App::delete('/v1/account/mfa/:provider')
|
App::delete('/v1/account/mfa/:provider')
|
||||||
->desc('Delete Authenticator')
|
->desc('Delete Authenticator')
|
||||||
->groups(['api', 'account', 'mfa'])
|
->groups(['api', 'account'])
|
||||||
->label('event', 'users.[userId].delete.mfa')
|
->label('event', 'users.[userId].delete.mfa')
|
||||||
->label('scope', 'account')
|
->label('scope', 'accounts.write')
|
||||||
->label('audits.event', 'user.update')
|
->label('audits.event', 'user.update')
|
||||||
->label('audits.resource', 'user/{response.$id}')
|
->label('audits.resource', 'user/{response.$id}')
|
||||||
->label('audits.userId', '{response.$id}')
|
->label('audits.userId', '{response.$id}')
|
||||||
|
@ -3459,7 +3459,7 @@ App::delete('/v1/account/mfa/:provider')
|
||||||
App::post('/v1/account/mfa/challenge')
|
App::post('/v1/account/mfa/challenge')
|
||||||
->desc('Create MFA Challenge')
|
->desc('Create MFA Challenge')
|
||||||
->groups(['api', 'account', 'mfa'])
|
->groups(['api', 'account', 'mfa'])
|
||||||
->label('scope', 'account')
|
->label('scope', 'accounts.write')
|
||||||
->label('event', 'users.[userId].challenges.[challengeId].create')
|
->label('event', 'users.[userId].challenges.[challengeId].create')
|
||||||
->label('auth.type', 'createChallenge')
|
->label('auth.type', 'createChallenge')
|
||||||
->label('audits.event', 'challenge.create')
|
->label('audits.event', 'challenge.create')
|
||||||
|
@ -3554,7 +3554,7 @@ App::post('/v1/account/mfa/challenge')
|
||||||
App::put('/v1/account/mfa/challenge')
|
App::put('/v1/account/mfa/challenge')
|
||||||
->desc('Create MFA Challenge (confirmation)')
|
->desc('Create MFA Challenge (confirmation)')
|
||||||
->groups(['api', 'account', 'mfa'])
|
->groups(['api', 'account', 'mfa'])
|
||||||
->label('scope', 'account')
|
->label('scope', 'accounts.write')
|
||||||
->label('event', 'users.[userId].sessions.[tokenId].create')
|
->label('event', 'users.[userId].sessions.[tokenId].create')
|
||||||
->label('audits.event', 'challenges.update')
|
->label('audits.event', 'challenges.update')
|
||||||
->label('audits.resource', 'user/{response.userId}')
|
->label('audits.resource', 'user/{response.userId}')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Appwrite\Auth\Auth;
|
use Appwrite\Auth\Auth;
|
||||||
|
use Appwrite\Auth\MFA\Challenge;
|
||||||
use Appwrite\Auth\Validator\Password;
|
use Appwrite\Auth\Validator\Password;
|
||||||
use Appwrite\Auth\Validator\Phone;
|
use Appwrite\Auth\Validator\Phone;
|
||||||
use Appwrite\Detector\Detector;
|
use Appwrite\Detector\Detector;
|
||||||
|
@ -1422,6 +1423,131 @@ App::patch('/v1/users/:userId/targets/:targetId')
|
||||||
->dynamic($target, Response::MODEL_TARGET);
|
->dynamic($target, Response::MODEL_TARGET);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
App::patch('/v1/users/:userId/mfa')
|
||||||
|
->desc('Update MFA')
|
||||||
|
->groups(['api', 'users'])
|
||||||
|
->label('event', 'users.[userId].update.mfa')
|
||||||
|
->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', 'updateMfa')
|
||||||
|
->label('sdk.description', '/docs/references/users/update-user-mfa.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('mfa', null, new Boolean(), 'Enable or disable MFA.')
|
||||||
|
->inject('response')
|
||||||
|
->inject('dbForProject')
|
||||||
|
->inject('queueForEvents')
|
||||||
|
->action(function (string $userId, bool $mfa, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||||
|
|
||||||
|
$user = $dbForProject->getDocument('users', $userId);
|
||||||
|
|
||||||
|
if ($user->isEmpty()) {
|
||||||
|
throw new Exception(Exception::USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->setAttribute('mfa', $mfa);
|
||||||
|
|
||||||
|
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
|
||||||
|
|
||||||
|
$queueForEvents->setParam('userId', $user->getId());
|
||||||
|
|
||||||
|
$response->dynamic($user, Response::MODEL_USER);
|
||||||
|
});
|
||||||
|
|
||||||
|
App::get('/v1/users/:userId/providers')
|
||||||
|
->desc('List Providers')
|
||||||
|
->groups(['api', 'users'])
|
||||||
|
->label('scope', 'users.read')
|
||||||
|
->label('usage.metric', 'users.{scope}.requests.read')
|
||||||
|
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||||
|
->label('sdk.namespace', 'users')
|
||||||
|
->label('sdk.method', 'listProviders')
|
||||||
|
->label('sdk.description', '/docs/references/users/list-providers.md')
|
||||||
|
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||||
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||||
|
->label('sdk.response.model', Response::MODEL_MFA_PROVIDERS)
|
||||||
|
->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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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)
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->dynamic($providers, Response::MODEL_MFA_PROVIDERS);
|
||||||
|
});
|
||||||
|
|
||||||
|
App::delete('/v1/users/:userId/mfa/:provider')
|
||||||
|
->desc('Delete Authenticator')
|
||||||
|
->groups(['api', 'users'])
|
||||||
|
->label('event', 'users.[userId].delete.mfa')
|
||||||
|
->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_SESSION, APP_AUTH_TYPE_JWT])
|
||||||
|
->label('sdk.namespace', 'users')
|
||||||
|
->label('sdk.method', 'deleteAuthenticator')
|
||||||
|
->label('sdk.description', '/docs/references/users/delete-mfa.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('provider', null, new WhiteList(['totp']), 'Provider.')
|
||||||
|
->param('otp', '', new Text(256), 'Valid verification token.')
|
||||||
|
->inject('requestTimestamp')
|
||||||
|
->inject('response')
|
||||||
|
->inject('dbForProject')
|
||||||
|
->inject('queueForEvents')
|
||||||
|
->action(function (string $userId, string $provider, string $otp, ?\DateTime $requestTimestamp, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||||
|
$user = $dbForProject->getDocument('users', $userId);
|
||||||
|
|
||||||
|
if ($user->isEmpty()) {
|
||||||
|
throw new Exception(Exception::USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = match ($provider) {
|
||||||
|
'totp' => Challenge\TOTP::verify($user, $otp),
|
||||||
|
default => false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!$success) {
|
||||||
|
throw new Exception(Exception::USER_INVALID_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user->getAttribute('totp')) {
|
||||||
|
throw new Exception(Exception::GENERAL_UNKNOWN, 'TOTP not added.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user
|
||||||
|
->setAttribute('totp', false)
|
||||||
|
->setAttribute('totpVerification', false)
|
||||||
|
->setAttribute('totpSecret', null)
|
||||||
|
->setAttribute('totpBackup', null);
|
||||||
|
|
||||||
|
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
|
||||||
|
|
||||||
|
$queueForEvents->setParam('userId', $user->getId());
|
||||||
|
|
||||||
|
$response->noContent();
|
||||||
|
});
|
||||||
|
|
||||||
App::post('/v1/users/:userId/sessions')
|
App::post('/v1/users/:userId/sessions')
|
||||||
->desc('Create session')
|
->desc('Create session')
|
||||||
->groups(['api', 'users'])
|
->groups(['api', 'users'])
|
||||||
|
|
|
@ -566,7 +566,11 @@ App::init()
|
||||||
if ($mode !== APP_MODE_ADMIN) {
|
if ($mode !== APP_MODE_ADMIN) {
|
||||||
$minFactors = $project->getAttribute('minFactors') ?? 1;
|
$minFactors = $project->getAttribute('minFactors') ?? 1;
|
||||||
$mfaEnabled = $user->getAttribute('mfa', false);
|
$mfaEnabled = $user->getAttribute('mfa', false);
|
||||||
if ($mfaEnabled && $minFactors === 1) {
|
$hasVerifiedAuthenticator = $user->getAttribute('totpVerification', false);
|
||||||
|
$hasVerifiedEmail = $user->getAttribute('emailVerification', false);
|
||||||
|
$hasVerifiedPhone = $user->getAttribute('phoneVerification', false);
|
||||||
|
$hasMoreFactors = $hasVerifiedEmail || $hasVerifiedPhone || $hasVerifiedAuthenticator;
|
||||||
|
if ($mfaEnabled && $hasMoreFactors && $minFactors === 1) {
|
||||||
$minFactors = 2;
|
$minFactors = 2;
|
||||||
}
|
}
|
||||||
if (!in_array('mfa', $route->getGroups())) {
|
if (!in_array('mfa', $route->getGroups())) {
|
||||||
|
|
Loading…
Reference in a new issue