We've open-sourced passkeys-php, the WebAuthn server library we use at Report URI to protect logins with passkeys, security keys, and platform authenticators like Touch ID, Face ID, and Windows Hello.

It started as a set of local security fixes for our own production passkeys implementation. Now, rather than carrying those patches privately, we’re releasing them as a small, auditable, MIT-licensed PHP library for everyone else building normal passkey login flows.

To get started: composer require report-uri/passkeys-php
Packagist: https://packagist.org/packages/report-uri/passkeys-php

Why We Built It

Our passkeys-php is a maintained fork of the excellent lbuchs/WebAuthn, forked at upstream v2.2.0. We wanted to preserve what made that library appealing: it was small, lightweight, and understandable enough that you could actually read the code guarding your logins.

The catch was that the upstream is effectively dormant. When we had Report URI's passkeys integration penetration tested, the assessment surfaced several WebAuthn conformance issues. We wrote fixes and submitted them as PRs upstream, but they haven't been merged. Rather than carry a stack of local patches indefinitely — and leave everyone else on the same library exposed — we're shipping the fixes inline and in the open.

What We Fixed

Each fix is its own commit on main so you can audit exactly what changed and why if you'd like, but the summary is below. These were not cosmetic changes; they were the kinds of edge cases that matter when a library is responsible for deciding whether an authentication ceremony is valid.

  • Tighter origin check. The previous RP-ID match treated the RP ID as a substring suffix, so example.com would match the host evil-example.com. It
    now requires an exact match or a true subdomain.
  • Cross-origin rejection. Registration and authentication now reject ceremonies where clientDataJSON.crossOrigin === true, per WebAuthn Level 3.
  • Attestation none hardening. The none attestation statement must be an empty CBOR map, per WebAuthn §8.7. Non-empty maps are now rejected.
  • Backup flag validation. Authenticator data with the Backup State bit set but Backup Eligible unset is now rejected, per spec.
  • Token Binding rejection. Ceremonies asserting Token Binding are rejected, since the library doesn't implement it.

We Deleted Attestation

The headline change is that attestation verification is gone entirely; the library now supports only the none attestation format. Our penetration test and our own internal security reviews showed that serious risk was concentrated almost entirely in attestation-statement handling — the TPM, Packed, U2F, Android Key, Android SafetyNet and Apple formats, plus the FIDO Metadata Service plumbing and root-CA trust set. That code path is also the part of WebAuthn that our typical users don't use: browsers and platform authenticators issue attestation: "none" by default, and demanding attestation actively harms passkey UX and privacy.

So we removed it — over 1,100 lines of it. Now, getCreateArgs() always requests attestation: "none" (which the spec requires the client to honour by stripping the statement, whatever authenticator the user holds), and only fmt: "none" with an empty attStmt is accepted. The library is now positioned for the common case: SaaS-style passkey auth where the relying party only needs to know the user controls a credential bound to the RP — not which authenticator produced it. If you genuinely need enterprise attestation with a managed CA set, this isn't the library for you, and we think that's the right trade: a large, dangerous, rarely-exercised attack surface deleted instead of subtly-broken verifiers shipped to people who wouldn't enable them anyway.

Getting Started

The library autoloads under PSR-4 as ReportUri\Passkeys, with the main entry point aligned with the spec name:

use ReportUri\Passkeys\WebAuthn;
$server = new WebAuthn('My App', 'example.com');

There's a working registration and login demo in _test/ to get you going, and this is currently deployed on the Report URI production site so you can always test it there too!

Passkeys are one of the best things to happen to authentication in years, but only if the server side gets the verification right. That’s the part users never see, and the part a library has to get exactly right.

passkeys-php is our attempt to keep that code small, readable, auditable, and safe for the common case. Issues and PRs welcome.