Add documentation, remove 1FA routes
This commit is contained in:
parent
a71948edee
commit
ef64105e14
5 changed files with 7 additions and 199 deletions
|
@ -471,202 +471,6 @@ App::delete('/v1/account')
|
||||||
|
|
||||||
$response->noContent();
|
$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')
|
App::get('/v1/account/sessions')
|
||||||
->desc('List sessions')
|
->desc('List sessions')
|
||||||
->groups(['api', 'account'])
|
->groups(['api', 'account'])
|
||||||
|
@ -4046,8 +3850,8 @@ App::post('/v1/account/mfa/authenticators/webauthn')
|
||||||
->label('audits.userId', '{response.$id}')
|
->label('audits.userId', '{response.$id}')
|
||||||
->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')
|
||||||
->label('sdk.method', 'createMfaWebauthnAuthenticator')
|
->label('sdk.method', 'createWebauthnMfaAuthenticator')
|
||||||
->label('sdk.description', '/docs/references/account/create-mfa-webauthn-authenticator.md')
|
->label('sdk.description', '/docs/references/account/create-webauthn-mfa-authenticator.md')
|
||||||
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
->label('sdk.response.code', Response::STATUS_CODE_OK)
|
||||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||||
->label('sdk.response.model', Response::MODEL_WEBAUTHN_REGISTER_CHALLENGE)
|
->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.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_JWT])
|
||||||
->label('sdk.namespace', 'account')
|
->label('sdk.namespace', 'account')
|
||||||
->label('sdk.method', 'updateWebauthnMfaAuthenticator')
|
->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.code', Response::STATUS_CODE_OK)
|
||||||
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
|
||||||
->label('sdk.response.model', Response::MODEL_USER)
|
->label('sdk.response.model', Response::MODEL_USER)
|
||||||
|
|
|
@ -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.
|
1
docs/references/account/create-webauthn-mfa-challenge.md
Normal file
1
docs/references/account/create-webauthn-mfa-challenge.md
Normal file
|
@ -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.
|
|
@ -0,0 +1 @@
|
||||||
|
Verify an authenticator app after adding it using the [add authenticator](/docs/references/cloud/client-web/account#createWebauthnMfaAuthenticator) method.
|
1
docs/references/account/update-webauthn-mfa-challenge.md
Normal file
1
docs/references/account/update-webauthn-mfa-challenge.md
Normal file
|
@ -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.
|
Loading…
Reference in a new issue