From 5704d6c0f7625e51afe2a6835f96534a3dcb8960 Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 14 Jun 2026 18:33:20 +0200 Subject: [PATCH] fix(webauthn): re-serialize the credential after an assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Persist the updated PublicKeyCredentialSource (not just the sign_count column) so the next assertion validates against the latest counter — keeping the library's cloned-authenticator / counter-rollback detection effective. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Services/WebauthnService.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/Services/WebauthnService.php b/app/Services/WebauthnService.php index b246411..a90a5f6 100644 --- a/app/Services/WebauthnService.php +++ b/app/Services/WebauthnService.php @@ -143,7 +143,18 @@ class WebauthnService $validator = AuthenticatorAssertionResponseValidator::create($this->ceremony()->requestCeremony()); $updated = $validator->check($source, $assertion, $options, $this->rpId(), (string) $user->id); - $model->update(['sign_count' => $updated->counter, 'last_used_at' => now()]); + // Re-serialize the UPDATED record so the new sign counter is also embedded in + // public_key — otherwise the next assertion would validate against the stale + // registration-time counter and clone/rollback detection would never fire. + $updatedSource = $updated instanceof PublicKeyCredentialSource + ? $updated + : PublicKeyCredentialSource::fromCredentialRecord($updated); + + $model->update([ + 'public_key' => $serializer->serialize($updatedSource, 'json'), + 'sign_count' => $updated->counter, + 'last_used_at' => now(), + ]); session()->forget('webauthn.login'); return true;