- Integrate WebAuthn library for passkey authentication workflows, including assertion verification and improved error handling. - Add support for reCAPTCHA token validation across multiple endpoints for enhanced security. - Extend OpenAPI schema to document new fields and restructured payloads. - Add unit tests for WebAuthn flows, permission initialization, and route validation to ensure robustness and accuracy.
121 lines
4.4 KiB
PHP
121 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
use Exception;
|
|
use Symfony\Component\Serializer\SerializerInterface;
|
|
use Symfony\Component\Uid\Uuid;
|
|
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
|
use Webauthn\AuthenticatorAssertionResponse;
|
|
use Webauthn\AuthenticatorAssertionResponseValidator;
|
|
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
|
|
use Webauthn\Denormalizer\WebauthnSerializerFactory;
|
|
use Webauthn\PublicKeyCredential;
|
|
use Webauthn\PublicKeyCredentialRequestOptions;
|
|
use Webauthn\PublicKeyCredentialSource;
|
|
use Webauthn\TrustPath\EmptyTrustPath;
|
|
use objects\passkeys_o;
|
|
|
|
class webauthn
|
|
{
|
|
private AuthenticatorAssertionResponseValidator $validator;
|
|
private SerializerInterface $serializer;
|
|
|
|
public function __construct()
|
|
{
|
|
$factory = new CeremonyStepManagerFactory();
|
|
|
|
// In a real scenario, we should set allowed origins.
|
|
// We use the $_SERVER['HTTP_ORIGIN'] or similar.
|
|
$rpId = parse_url((string)($_SERVER['HTTP_ORIGIN'] ?? ''), PHP_URL_HOST) ?: ($_SERVER['SERVER_NAME'] ?? 'localhost');
|
|
$factory->setAllowedOrigins(['https://' . $rpId, 'http://' . $rpId]);
|
|
|
|
$this->validator = new AuthenticatorAssertionResponseValidator(
|
|
$factory->requestCeremony()
|
|
);
|
|
|
|
$attestationStatementSupportManager = new AttestationStatementSupportManager();
|
|
|
|
$serializerFactory = new WebauthnSerializerFactory($attestationStatementSupportManager);
|
|
$this->serializer = $serializerFactory->create();
|
|
}
|
|
|
|
/**
|
|
* Verify a WebAuthn assertion
|
|
*
|
|
* @param string $assertionJson The JSON string from the client
|
|
* @param string $challengeToken The hex challenge token stored in our DB
|
|
* @param passkeys_o $passkey The passkey object from our DB
|
|
* @param string $host The host (rpId) to verify against
|
|
* @return bool
|
|
*/
|
|
public function verifyAssertion(
|
|
string $assertionJson,
|
|
string $challengeToken,
|
|
passkeys_o $passkey,
|
|
string $host
|
|
): bool {
|
|
try {
|
|
$publicKeyCredential = $this->serializer->deserialize($assertionJson, PublicKeyCredential::class, 'json');
|
|
$response = $publicKeyCredential->response;
|
|
|
|
if (!$response instanceof AuthenticatorAssertionResponse) {
|
|
throw new Exception('Not an assertion response');
|
|
}
|
|
|
|
// Create PublicKeyCredentialSource from passkey object
|
|
$source = $this->createSourceFromObject($passkey);
|
|
|
|
// Prepare challenge (Base64URL without padding)
|
|
$rawChallenge = hex2bin($challengeToken);
|
|
$challenge = rtrim(strtr(base64_encode($rawChallenge), '+/', '-_'), '=');
|
|
|
|
// Create PublicKeyCredentialRequestOptions
|
|
$options = PublicKeyCredentialRequestOptions::create(
|
|
$challenge,
|
|
$host
|
|
);
|
|
|
|
// Check
|
|
$this->validator->check(
|
|
$source,
|
|
$response,
|
|
$options,
|
|
$host,
|
|
$source->userHandle
|
|
);
|
|
|
|
// If it didn't throw, it's valid. Update sign count.
|
|
// Note: validator updates source counter in v5
|
|
$passkey->sign_count->set($source->counter);
|
|
|
|
return true;
|
|
} catch (Exception $e) {
|
|
// Log error
|
|
error_log('WebAuthn verification failed: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function createSourceFromObject(passkeys_o $passkey): PublicKeyCredentialSource
|
|
{
|
|
$transports = $passkey->transports->value() ?: [];
|
|
|
|
// Decode Base64URL credential ID and public key to binary as expected by the WebAuthn lib
|
|
$credential_id_bin = base64_decode(strtr($passkey->credential_id->value(), '-_', '+/'));
|
|
$public_key_bin = base64_decode(strtr($passkey->public_key->value(), '-_', '+/'));
|
|
|
|
return new PublicKeyCredentialSource(
|
|
$credential_id_bin, // publicKeyCredentialId (binary)
|
|
'public-key',
|
|
$transports,
|
|
'none', // attestationType
|
|
new EmptyTrustPath(),
|
|
Uuid::fromString('00000000-0000-0000-0000-000000000000'), // aaguid
|
|
$public_key_bin, // credentialPublicKey (binary)
|
|
(string)$passkey->user_id->value(), // userHandle
|
|
(int)$passkey->sign_count->value()
|
|
);
|
|
}
|
|
}
|