Refactor things into it's own class

This commit is contained in:
Bradley Schofield
2024-06-21 22:42:44 +09:00
parent 976411d853
commit a080e7c583
4 changed files with 534 additions and 101 deletions
+288 -101
View File
@@ -5,6 +5,7 @@ use Appwrite\Auth\Auth;
use Appwrite\Auth\MFA\Challenge;
use Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Auth\MFA\Type\WebAuthn;
use Appwrite\Auth\OAuth2\Exception as OAuth2Exception;
use Appwrite\Auth\Phrase;
use Appwrite\Auth\Validator\Password;
@@ -417,7 +418,7 @@ App::post('/v1/account/webauthn')
->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_USER)
->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)
@@ -471,71 +472,26 @@ App::post('/v1/account/webauthn')
}
}
// TODO: Check there isn't an existing challenge for this userId & email
// TODO: Add a challenge expiry time
$platforms = $project->getAttribute('platforms', []);
$platformName = '';
$platformId = '';
//TODO: Use SDK headers to determine the platform
// Fallback to any web platform that matches the domain
foreach ($platforms as $platform) {
if ($platform['type'] === 'web' && $platform['hostname'] == $request->getHostname()) {
$platformName = $platform['name'];
$platformId = $platform['hostname'];
break;
}
}
// Console
if ($project->getId() === 'console') {
$platformName = 'Appwrite';
$platformId = 'localhost'; // TODO: Replace with hostname from _APP_DOMAIN
}
// If still no platform, throw.
if (empty($platformName)) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'No platform found for this domain.');
}
// Generate rpEntity from current platform
$rpEntity = PublicKeyCredentialRpEntity::create(
$platformName, //Name
$platformId, //ID
null //Icon
);
// Generate userEntity
$userId = $userId == 'unique()' ? ID::unique() : $userId;
$userEntity = PublicKeyCredentialUserEntity::create(
$name,
$userId,
$name,
null
);
$challenge = random_bytes(16);
$publicKeyCredentialCreationOptions =
PublicKeyCredentialCreationOptions::create(
$rpEntity,
$userEntity,
$challenge
);
$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([
'$id' => ID::unique(),
'type' => 'user_creation',
'rp' => json_encode($publicKeyCredentialCreationOptions->rp),
'user' => json_encode($publicKeyCredentialCreationOptions->user),
'challenge' => Base64UrlSafe::encodeUnpadded($challenge),
'userId' => $userId,
'pubKeyCredParams' => json_encode($publicKeyCredentialCreationOptions->pubKeyCredParams),
'expire' => $expire,
]);
$webauthnDocument = new Document(
array_merge([
'$id' => ID::unique(),
'type' => 'user_creation',
'expire' => $expire,
], $challenge->jsonSerialize())
);
if (!empty($email)) {
$webauthnDocument->setAttribute('email', $email);
@@ -580,34 +536,12 @@ App::put('/v1/account/webauthn')
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid challenge.');
}
// Validate Challenge Response
$publicKeyCredential = $publicKeyCredentialLoader->load($challengeResponse);
$publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::create(
PublicKeyCredentialRpEntity::create(
$challengeDocument['rp']['name'], //Name
$challengeDocument['rp']['id'], //ID
null //Icon
),
PublicKeyCredentialUserEntity::create(
$challengeDocument['user']['name'],
$challengeDocument['user']['id'],
$challengeDocument['user']['displayName'],
),
Base64UrlSafe::decode($challengeDocument['challenge']),
);
$publicKeyCredentialSource = [];
$webauthn = new WebAuthn();
$publicKeyCredentials = null;
try {
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
$publicKeyCredential->response,
$publicKeyCredentialCreationOptions,
$challengeDocument['rp']['id'], // Replace with platform domain or origin
['localhost'] // Remove when we can.
);
} catch (\Throwable $th) {
throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid challenge response.');
$publicKeyCredentials = $webauthn->verifyRegisterChallenge($challengeDocument->getArrayCopy(), $challengeResponse);
} catch (\Exception $e) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
// Create a user account with webauthn enabled.
@@ -642,19 +576,15 @@ App::put('/v1/account/webauthn')
$createdUser = Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
// Create Authenticator
$dbForProject->createDocument('credentialSources', new Document([
'userInternalId' => $createdUser->getInternalId(),
'publicKeyCredentialId' => Base64UrlSafe::encodeUnpadded($publicKeyCredentialSource->publicKeyCredentialId),
'type' => $publicKeyCredentialSource->type,
'transports' => $publicKeyCredentialSource->transports,
'attestationType' => $publicKeyCredentialSource->attestationType,
'aaguid' => $publicKeyCredentialSource->aaguid->__toString(),
'trustPath' => json_encode($publicKeyCredentialSource->trustPath),
'credentialPublicKey' => Base64UrlSafe::encodeUnpadded($publicKeyCredentialSource->credentialPublicKey),
'userHandle' => Base64UrlSafe::encodeUnpadded($publicKeyCredentialSource->userHandle),
'counter' => $publicKeyCredentialSource->counter,
]));
$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);
@@ -4321,6 +4251,130 @@ App::put('/v1/account/mfa/authenticators/:type')
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::post('/v1/account/mfa/authenticators/webauthn')
->desc('Add WebAuthn Authenticator')
->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', 'createMfaWebauthnAuthenticator')
->label('sdk.description', '/docs/references/account/create-mfa-webauthn-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)
->label('sdk.offline.model', '/account')
->label('sdk.offline.key', 'current')
->inject('request')
->inject('response')
->inject('project')
->inject('user')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (Request $request, Response $response, Document $project, Document $user, Database $dbForProject, Event $queueForEvents) {
// Clean up any previous challenges not completed
$authenticators = array_filter($user->getAttribute('authenticators', []), fn ($authenticator) => $authenticator['type'] === Type::WEBAUTHN);
foreach ($authenticators as $authenticator) {
if (empty($authenticator['verified'])) {
$dbForProject->deleteDocument('authenticators', $authenticator['id']);
}
}
$webauthn = new WebAuthn();
$relyingParty = $webauthn->createRelyingParty($project, $request);
$userEntity = $webauthn->createUserEntity($user);
$challenge = $webauthn->createRegisterChallenge($relyingParty, $userEntity, 60 * 5);
$authenticator = new Document([
'$id' => ID::unique(),
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Type::WEBAUTHN,
'verified' => false,
'data' => $challenge->jsonSerialize(),
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
]
]);
$authenticator = $dbForProject->createDocument('authenticators', $authenticator);
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents->setParam('userId', $user->getId());
$model = new Document($challenge->jsonSerialize());
$response->dynamic($model, Response::MODEL_WEBAUTHN_REGISTER_CHALLENGE);
});
App::put('/v1/account/mfa/authenticators/webauthn')
->desc('Verify WebAuthn Authenticator')
->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', 'updateWebauthnMfaAuthenticator')
->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('challengeResponse', '', new Text(8192), 'Valid verification token.')
->inject('response')
->inject('user')
->inject('session')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $challengeResponse, Response $response, Document $user, Document $session, Database $dbForProject, Event $queueForEvents) {
/** @var ?Document $authenticator */
$authenticator = null;
foreach ($user->getAttribute('authenticators', []) as $auth) {
if ($auth['type'] === Type::WEBAUTHN && empty($auth['verified'])) {
$authenticator = $auth;
}
};
if ($authenticator === null) {
throw new Exception(Exception::USER_AUTHENTICATOR_NOT_FOUND);
}
$webauthn = new WebAuthn();
$challenge = $authenticator->getAttribute('data');
$publicKeyCredentials = null;
try {
$publicKeyCredentials = $webauthn->verifyRegisterChallenge($challenge, $challengeResponse);
} catch (\Exception $e) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
$authenticator->setAttribute('verified', true);
$authenticator->setAttribute('data', $publicKeyCredentials->jsonSerialize());
$dbForProject->updateDocument('authenticators', $authenticator->getId(), $authenticator);
$factors = $session->getAttribute('factors', []);
$factors[] = Type::WEBAUTHN;
$factors = \array_unique($factors);
$session->setAttribute('factors', $factors);
$dbForProject->updateDocument('sessions', $session->getId(), $session);
$queueForEvents->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_ACCOUNT);
});
App::post('/v1/account/mfa/recovery-codes')
->desc('Create MFA Recovery Codes')
->groups(['api', 'account'])
@@ -4769,6 +4823,139 @@ App::put('/v1/account/mfa/challenge')
$response->dynamic($session, Response::MODEL_SESSION);
});
App::post('/v1/account/mfa/challenge/webauthn')
->desc('Create WebAuthn MFA Challenge')
->groups(['api', 'account', 'mfa'])
->label('scope', 'account')
->label('event', 'users.[userId].challenges.[challengeId].create')
->label('audits.event', 'challenge.create')
->label('audits.resource', 'user/{response.userId}')
->label('audits.userId', '{response.userId}')
->label('sdk.auth', [])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createWebauthnMfaChallenge')
->label('sdk.description', '/docs/references/account/create-webauthn-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_WEBAUTHN_LOGIN_CHALLENGE)
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},token:{param-token}')
->inject('response')
->inject('dbForProject')
->inject('user')
->inject('locale')
->inject('project')
->inject('request')
->inject('queueForEvents')
->action(function (Response $response, Database $dbForProject, Document $user, Locale $locale, Document $project, Request $request, Event $queueForEvents) {
$expire = DateTime::addSeconds(new \DateTime(), Auth::TOKEN_EXPIRATION_WEBAUTHN);
$webauthn = new WebAuthn();
$allowedCredentials = $webauthn->getAllowedCredentials($user);
if (empty($allowedCredentials)) {
//todo: add exception
}
$relyingParty = $webauthn->createRelyingParty($project, $request);
$webAuthnChallenge = $webauthn->createLoginChallenge($relyingParty, $allowedCredentials, Auth::TOKEN_EXPIRATION_WEBAUTHN);
// Store challenge
$challenge = new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'type' => Type::WEBAUTHN,
'code' => Base64UrlSafe::encodeUnpadded($webAuthnChallenge->challenge),
'expire' => $expire,
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
]);
$challenge = $dbForProject->createDocument('challenges', $challenge);
$queueForEvents
->setParam('userId', $user->getId())
->setParam('challengeId', $challenge->getId());
// Send challenge
$response->dynamic(new Document(array_merge(
[
'$id' => $challenge->getId(),
],
$webAuthnChallenge->jsonSerialize()
)), Response::MODEL_WEBAUTHN_LOGIN_CHALLENGE);
});
App::put('/v1/account/mfa/webauthn/challenge')
->desc('Create WebAuthn MFA Challenge (confirmation)')
->groups(['api', 'account', 'mfa'])
->label('scope', 'account')
->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', 'updateWebauthnMfaChallenge')
->label('sdk.description', '/docs/references/account/update-webauthn-mfa-challenge.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_SESSION)
->label('abuse-limit', 10)
->label('abuse-key', 'userId:{param-userId}')
->param('challengeId', '', new Text(256), 'ID of the challenge.')
->param('challengeResponse', '', new Text(8192), 'Valid verification token.')
->inject('project')
->inject('response')
->inject('user')
->inject('session')
->inject('dbForProject')
->inject('queueForEvents')
->action(function (string $challengeId, string $challengeResponse, Document $project, Response $response, Document $user, Document $session, Database $dbForProject, Event $queueForEvents) {
$challenge = $dbForProject->getDocument('challenges', $challengeId);
if ($challenge->isEmpty()) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
$authenticators = array_filter($user->getAttribute('authenticators', []), function ($auth) {
return $auth['type'] === Type::WEBAUTHN && !empty($auth['verified']);
});
$webauthn = new WebAuthn();
// Find authenticator used
$authenticator = null;
foreach ($authenticators as $auth) {
$data = $auth['data'];
if ($data['credentialId'] === $challengeResponse['id']) {
$authenticator = $auth;
break;
}
}
if ($authenticator === null) {
throw new Exception(Exception::USER_AUTHENTICATOR_NOT_FOUND);
}
// Check challenge
$publicKeyCredential = null;
try {
$publicKeyCredential = $webauthn->verifyLoginChallenge($challenge->getArrayCopy(), $challengeResponse, $authenticators);
} catch (\Exception $e) {
throw new Exception(Exception::USER_INVALID_TOKEN);
}
// Update authenticator as counter has changed
// Update Session
});
App::post('/v1/account/targets/push')
->desc('Create push target')
->groups(['api', 'account'])
+1
View File
@@ -76,6 +76,7 @@ class Auth
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
public const TOKEN_EXPIRATION_OTP = 60 * 15; /* 15 minutes */
public const TOKEN_EXPIRATION_WEBAUTHN = 60 * 5; /* 5 minutes */
public const TOKEN_EXPIRATION_GENERIC = 60 * 15; /* 15 minutes */
/**
+1
View File
@@ -13,6 +13,7 @@ abstract class Type
public const EMAIL = 'email';
public const PHONE = 'phone';
public const RECOVERY_CODE = 'recoveryCode';
public const WEBAUTHN = 'webauthn';
public function setLabel(string $label): self
{
+244
View File
@@ -0,0 +1,244 @@
<?php
namespace Appwrite\Auth\MFA\Type;
use Appwrite\Auth\MFA\Type;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Request;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Utopia\App;
use Utopia\Database\Document;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
class WebAuthn extends Type
{
protected PublicKeyCredentialLoader $publicKeyCredentialLoader;
protected AuthenticatorAttestationResponseValidator $authenticatorAttestationResponseValidator;
protected AuthenticatorAssertionResponseValidator $authenticatiorAssertionResponseValdiator;
public function __construct()
{
$attestationSupportManager = AttestationStatementSupportManager::create();
$attestationObjectLoader = AttestationObjectLoader::create(
$attestationSupportManager
);
$this->publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);
$this->authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
$attestationSupportManager
);
$this->authenticatiorAssertionResponseValdiator = AuthenticatorAssertionResponseValidator::create();
}
public static function createRelyingParty(Document $project, Request $request): PublicKeyCredentialRpEntity
{
// Calculate Relying Party ID and Name
$platforms = $project->getAttribute('platforms', []);
$platformName = '';
$platformId = '';
// Detect platform and set platform name and id for Relying Party.
switch ($request->getHeader('x-sdk-name', '')) {
case 'Flutter':
$packageName = explode('/', $request->getHeader('user-agent', ''))[0] ?? '';
foreach ($platforms as $platform) {
if (str_starts_with($platform['type'], 'flutter') && $platform['key'] === $packageName) {
$platformName = $platform['name'];
$platformId = $platform['hostname'];
break;
}
}
break;
case 'Apple':
$packageName = explode('/', $request->getHeader('user-agent', ''))[0] ?? '';
foreach ($platforms as $platform) {
if (str_starts_with($platform['type'], 'apple') && $platform['key'] === $packageName) {
$platformName = $platform['name'];
$platformId = $platform['hostname'];
break;
}
}
break;
case 'Android':
$packageName = explode('/', $request->getHeader('user-agent', ''))[0] ?? '';
foreach ($platforms as $platform) {
if ($platform['type'] === 'android' && $platform['key'] === $packageName) {
$platformName = $platform['name'];
$platformId = $platform['hostname'];
break;
}
}
break;
case 'Web':
default:
// Fallback to any web platform that matches the domain
foreach ($platforms as $platform) {
if ($platform['type'] === 'web' && $platform['hostname'] == $request->getHostname()) {
$platformName = $platform['name'];
$platformId = $platform['hostname'];
break;
}
}
break;
}
// Console
if ($project->getId() === 'console') {
$platformName = 'Appwrite';
$platformId = App::getEnv('_APP_DOMAIN', '');
}
return new PublicKeyCredentialRpEntity(
$platformName,
$platformId
);
}
public static function createUserEntity(Document $user): PublicKeyCredentialUserEntity
{
$name = $user->getAttribute('name') ?? $user->getAttribute('email');
return new PublicKeyCredentialUserEntity(
$name,
$user->getId(),
$name,
);
}
public static function createRegisterChallenge(PublicKeyCredentialRpEntity $rpEntity, PublicKeyCredentialUserEntity $userEntity, int $timeout): PublicKeyCredentialCreationOptions
{
return PublicKeyCredentialCreationOptions::create(
rp: $rpEntity,
user: $userEntity,
challenge: random_bytes(32),
timeout: $timeout
);
}
public static function createLoginChallenge(PublicKeyCredentialRpEntity $rpEntity, array $allowedCredentials, int $timeout): PublicKeyCredentialRequestOptions
{
return PublicKeyCredentialRequestOptions::create(
rpId: $rpEntity->id,
userVerification: PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_DEFAULT,
challenge: random_bytes(32),
timeout: $timeout,
allowCredentials: $allowedCredentials
);
}
/**
* Get all allowed credentials for a user
*
* @param Document $user
* @return PublicKeyCredentialSource[]
*/
public static function getAllowedCredentials(Document $user): array
{
$authenticators = self::getAuthenticatorsFromUser($user);
$authenticators = array_filter($authenticators, function ($authenticator) {
/** @var Document $authenticator */
return $authenticator->getAttribute('verified') === true;
});
if (empty($authenticators)) {
return [];
}
return array_map(function ($authenticator) {
/** @var Document $authenticator */
return PublicKeyCredentialSource::createFromArray($authenticator->getArrayCopy());
}, $authenticators);
}
/**
* @throws \Throwable
*/
public function verifyRegisterChallenge(array $challenge, string $challengeResponse): PublicKeyCredentialSource
{
$publicKeyCredential = $this->publicKeyCredentialLoader->load($challengeResponse);
$relyingParty = PublicKeyCredentialRpEntity::create(
$challenge['rp']['name'],
$challenge['rp']['id']
);
$userEntity = PublicKeyCredentialUserEntity::create(
$challenge['user']['name'],
$challenge['user']['id'],
$challenge['user']['displayName'],
);
$publicKeyCreationOptions = PublicKeyCredentialCreationOptions::create(
$relyingParty,
$userEntity,
Base64UrlSafe::decode($challenge['challenge']),
);
return $this->authenticatorAttestationResponseValidator->check(
$publicKeyCredential->response,
$publicKeyCreationOptions,
$challenge['rp']['id'],
App::isDevelopment() ? ['localhost'] : [],
);
}
/**
* @throws \Throwable
*/
public function verifyLoginChallenge(array $challenge, string $challengeResponse, string $hostname, PublicKeyCredentialSource $authenticatorPublicKey): PublicKeyCredentialSource
{
$publicKeyCredential = $this->publicKeyCredentialLoader->load($challengeResponse);
if (!$publicKeyCredential->response instanceof AuthenticatorAssertionResponse) {
throw new Exception('Invalid response');
}
$requestOptions = PublicKeyCredentialRequestOptions::create(
rpId: $challenge['rp']['id'],
userVerification: PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_DEFAULT,
challenge: Base64UrlSafe::decode($challenge['challenge']),
timeout: $challenge['timeout'],
allowCredentials: $challenge['allowCredentials']
);
return $this->authenticatiorAssertionResponseValdiator->check(
credentialId: $authenticatorPublicKey,
authenticatorAssertionResponse: $publicKeyCredential->response,
publicKeyCredentialRequestOptions: $requestOptions,
request: $hostname,
userHandle: $authenticatorPublicKey->userHandle
);
}
public static function getAuthenticatorsFromUser(Document $user): ?array
{
$authenticators = array_filter($user->getAttribute('authenticators', []), function ($authenticator) {
/** @var Document $authenticator */
return $authenticator->getAttribute('type') === Type::WEBAUTHN;
});
if (empty($authenticators)) {
return null;
}
return $authenticators;
}
}