From ef64105e146b1f39562e647b865524d75d97a161 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Mon, 8 Jul 2024 13:34:20 +0900 Subject: [PATCH] Add documentation, remove 1FA routes --- app/controllers/api/account.php | 202 +----------------- .../create-webauthn-mfa-authenticator.md | 1 + .../account/create-webauthn-mfa-challenge.md | 1 + .../update-webauthn-mfa-authenticator.md | 1 + .../account/update-webauthn-mfa-challenge.md | 1 + 5 files changed, 7 insertions(+), 199 deletions(-) create mode 100644 docs/references/account/create-webauthn-mfa-authenticator.md create mode 100644 docs/references/account/create-webauthn-mfa-challenge.md create mode 100644 docs/references/account/update-webauthn-mfa-authenticator.md create mode 100644 docs/references/account/update-webauthn-mfa-challenge.md diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index fd250e1790..8e2efe9ba5 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -471,202 +471,6 @@ App::delete('/v1/account') $response->noContent(); }); -App::post('/v1/account/webauthn') - ->desc('Create Webauthn Account') - ->groups(['api', 'account', 'auth']) - ->label('scope', 'sessions.write') - ->label('auth.type', 'webauthn') - ->label('audits.event', 'user.create') - ->label('audits.resource', 'user/{response.$id}') - ->label('audits.userId', '{response.$id}') - ->label('sdk.auth', []) - ->label('sdk.namespace', 'account') - ->label('sdk.method', 'webauthnCreate') - ->label('sdk.description', '/docs/references/account/webauthn-create.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_WEBAUTHN_REGISTER_CHALLENGE) - ->label('abuse-limit', 10) - ->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') - ->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true) - ->param('email', '', new Email(), 'User email.', true) - ->inject('request') - ->inject('response') - ->inject('user') - ->inject('project') - ->inject('dbForProject') - ->inject('queueForEvents') - ->inject('hooks') - ->action(function (string $userId, string $name, string $email, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { - $email = \strtolower($email ?? ''); - if ('console' === $project->getId()) { - if (empty($email)) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Parameter "email" is requried.'); // Possibly invalid exception, check later. - } - - $whitelistEmails = $project->getAttribute('authWhitelistEmails'); - $whitelistIPs = $project->getAttribute('authWhitelistIPs'); - - if (!empty($whitelistEmails) && !\in_array($email, $whitelistEmails) && !\in_array(strtoupper($email), $whitelistEmails)) { - throw new Exception(Exception::USER_EMAIL_NOT_WHITELISTED); - } - - if (!empty($whitelistIPs) && !\in_array($request->getIP(), $whitelistIPs)) { - throw new Exception(Exception::USER_IP_NOT_WHITELISTED); - } - } - - $limit = $project->getAttribute('auths', [])['limit'] ?? 0; - - if ($limit !== 0) { - $total = $dbForProject->count('users', max: APP_LIMIT_USERS); - - if ($total >= $limit) { - if ('console' === $project->getId()) { - throw new Exception(Exception::USER_CONSOLE_COUNT_EXCEEDED); - } - throw new Exception(Exception::USER_COUNT_EXCEEDED); - } - } - - // Makes sure this email is not already used in another identity - if (!empty($email)) { - $identityWithMatchingEmail = $dbForProject->findOne('identities', [ - Query::equal('providerEmail', [$email]), - ]); - if ($identityWithMatchingEmail !== false && !$identityWithMatchingEmail->isEmpty()) { - throw new Exception(Exception::GENERAL_BAD_REQUEST); /** Return a generic bad request to prevent exposing existing accounts */ - } - } - - $userId = $userId == 'unique()' ? ID::unique() : $userId; - - $webauthn = new WebAuthn(); - $relyingParty = $webauthn->createRelyingParty($project, $request); - $userEntity = $webauthn->createUserEntity(new Document([ - 'name' => $name, - 'email' => $email, - 'userId' => $userId, - ])); - $challenge = $webauthn->createRegisterChallenge($relyingParty, $userEntity, 60 * 5); - - $expire = DateTime::addSeconds(new \DateTime(), 60 * 5); - - $webauthnDocument = new Document( - array_merge([ - '$id' => ID::unique(), - 'type' => 'user_creation', - 'expire' => $expire, - ], $challenge) - ); - - if (!empty($email)) { - $webauthnDocument->setAttribute('email', $email); - } - - $dbForProject->createDocument('webauthnChallenges', $webauthnDocument); - - $response->dynamic($webauthnDocument, Response::MODEL_WEBAUTHN_REGISTER_CHALLENGE); - }); - -App::put('/v1/account/webauthn') - ->desc('Create WebAuthn User (confirmation)') - ->groups(['api', 'account', 'auth']) - ->label('event', 'users.[userId].create') - ->label('scope', 'sessions.write') - ->label('auth.type', 'webauthn') - ->label('audits.event', 'user.create') - ->label('audits.resource', 'user/{response.$id}') - ->label('audits.userId', '{response.$id}') - ->label('sdk.auth', []) - ->label('sdk.namespace', 'account') - ->label('sdk.method', 'webauthnVerify') - ->label('sdk.description', '/docs/references/account/webauthn-verify.md') - ->label('sdk.response.code', Response::STATUS_CODE_CREATED) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_USER) - ->label('abuse-limit', 10) - ->param('challengeId', '', new CustomId(), 'Challenge ID.') - ->param('challengeResponse', '', new text(8096), 'The response from the Webauthn client.') - ->inject('request') - ->inject('response') - ->inject('user') - ->inject('project') - ->inject('dbForProject') - ->inject('queueForEvents') - ->inject('hooks') - ->action(function (string $challengeId, string $challengeResponse, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Hooks $hooks) { - // Get the challenge from the database - $challengeDocument = Authorization::skip(fn () => $dbForProject->getDocument('webauthnChallenges', $challengeId)); - - if ($challengeDocument === false || $challengeDocument->isEmpty() || $challengeDocument->getAttribute('expire') < DateTime::now()) { - throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid challenge.'); - } - - $webauthn = new WebAuthn(); - $publicKeyCredentials = null; - try { - $publicKeyCredentials = $webauthn->verifyRegisterChallenge($challengeDocument->getArrayCopy(), $challengeResponse); - } catch (\Exception $e) { - throw new Exception(Exception::USER_INVALID_TOKEN); - } - - // Create a user account with webauthn enabled. - try { - $userId = $challengeDocument['userId']; - $email = $challengeDocument['email']; - $name = $challengeDocument['user']['name']; - $user->setAttributes([ - '$id' => $userId, - '$permissions' => [ - Permission::read(Role::any()), - Permission::update(Role::user($userId)), - Permission::delete(Role::user($userId)), - ], - 'email' => $email, - 'emailVerification' => false, - 'status' => true, - 'registration' => DateTime::now(), - 'reset' => false, - 'name' => $name, - 'mfa' => false, - 'prefs' => new \stdClass(), - 'sessions' => null, - 'tokens' => null, - 'memberships' => null, - 'authenticators' => null, - 'credentialSources' => null, - 'search' => implode(' ', [$userId, $email, $name]), - 'accessedAt' => DateTime::now(), - ]); - $user->removeAttribute('$internalId'); - $createdUser = Authorization::skip(fn () => $dbForProject->createDocument('users', $user)); - - // Create Authenticator - $dbForProject->createDocument( - 'credentialSources', - new Document( - array_merge( - ['userInternalId' => $createdUser->getInternalId()], - $publicKeyCredentials->jsonSerialize() - ) - ) - ); - - Authorization::skip(fn () => $dbForProject->deleteDocument('webauthnChallenges', $challengeId)); - } catch (Duplicate) { - throw new Exception(Exception::USER_ALREADY_EXISTS); - } - - $queueForEvents - ->setParam('userId', $user->getId()) - ; - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($user, Response::MODEL_ACCOUNT); - }); - App::get('/v1/account/sessions') ->desc('List sessions') ->groups(['api', 'account']) @@ -4046,8 +3850,8 @@ App::post('/v1/account/mfa/authenticators/webauthn') ->label('audits.userId', '{response.$id}') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') - ->label('sdk.method', 'createMfaWebauthnAuthenticator') - ->label('sdk.description', '/docs/references/account/create-mfa-webauthn-authenticator.md') + ->label('sdk.method', 'createWebauthnMfaAuthenticator') + ->label('sdk.description', '/docs/references/account/create-webauthn-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_WEBAUTHN_REGISTER_CHALLENGE) @@ -4109,7 +3913,7 @@ App::put('/v1/account/mfa/authenticators/webauthn') ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT]) ->label('sdk.namespace', 'account') ->label('sdk.method', 'updateWebauthnMfaAuthenticator') - ->label('sdk.description', '/docs/references/account/update-mfa-authenticator.md') + ->label('sdk.description', '/docs/references/account/update-webauthn-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/create-webauthn-mfa-authenticator.md b/docs/references/account/create-webauthn-mfa-authenticator.md new file mode 100644 index 0000000000..0326c06d46 --- /dev/null +++ b/docs/references/account/create-webauthn-mfa-authenticator.md @@ -0,0 +1 @@ +Add an webauthn authenticator to be used as an MFA factor. Verify the authenticator using the [verify webauthn authenticator](/docs/references/cloud/client-web/account#updateWebauthnMfaChallenge) method. \ No newline at end of file diff --git a/docs/references/account/create-webauthn-mfa-challenge.md b/docs/references/account/create-webauthn-mfa-challenge.md new file mode 100644 index 0000000000..077ad9dadb --- /dev/null +++ b/docs/references/account/create-webauthn-mfa-challenge.md @@ -0,0 +1 @@ +Begin the process of MFA verification after sign-in. Finish the flow with [updateWebauthnMfaChallenge](/docs/references/cloud/client-web/account#updateWebauthnMfaChallenge) method. \ No newline at end of file diff --git a/docs/references/account/update-webauthn-mfa-authenticator.md b/docs/references/account/update-webauthn-mfa-authenticator.md new file mode 100644 index 0000000000..6dbe5d2977 --- /dev/null +++ b/docs/references/account/update-webauthn-mfa-authenticator.md @@ -0,0 +1 @@ +Verify an authenticator app after adding it using the [add authenticator](/docs/references/cloud/client-web/account#createWebauthnMfaAuthenticator) method. \ No newline at end of file diff --git a/docs/references/account/update-webauthn-mfa-challenge.md b/docs/references/account/update-webauthn-mfa-challenge.md new file mode 100644 index 0000000000..acb01bff1a --- /dev/null +++ b/docs/references/account/update-webauthn-mfa-challenge.md @@ -0,0 +1 @@ +Complete the MFA challenge by providing the credential generated by the CredentialManager. To begin the flow, use [createWebauthnMfaChallenge](/docs/references/cloud/client-web/account#createWebauthnMfaChallenge) method. \ No newline at end of file