Files
api/services/nginx/app/classes/webauthn.php
T
Jeppe Bundgaard b291e959e0 Add WebAuthn integration, reCAPTCHA support, and new tests
- 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.
2026-02-24 09:27:51 +01:00

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()
);
}
}