How to Implement Passkey Authentication on Your PHP Website
Quick Summary: Passkeys leverage WebAuthn cryptographic key pairs to replace vulnerable passwords with biometric and device-based authentication. This guide provides a complete technical walkthrough for implementing passwordless passkey registration and login flows on a PHP backend with JavaScript.
Introduction
Passwords have long been the weakest link in digital identity and user authentication. Phishing campaigns, credential-stuffing attacks, and database breaches consistently compromise static text passwords. Passkeys solve this by shifting authentication to public-key cryptography via the WebAuthn standard.
When a user signs up or logs in with a passkey, their private key remains securely stored inside their device’s hardware enclave (protected by Touch ID, Face ID, Windows Hello, or a local PIN), while only the public key is shared with your server database. In this guide, we’ll walk through setting up a complete WebAuthn passkey implementation using PHP on the backend and native JavaScript on the frontend.
Prerequisites
Before starting your passkey implementation, ensure your environment meets these core requirements:
- PHP 8.1+ with the
gmporopensslextension enabled for cryptographic operations. - An HTTPS-enabled web server (WebAuthn requires a secure origin on all non-localhost domains).
- A PHP WebAuthn parsing library such as
lbuchs/WebAuthnorweb-token/jwt-frameworkinstalled via Composer.
How do you support passkey discovery using the well-known endpoint?
Modern browsers, operating systems, and password managers scan for a standardized manifest file to detect whether your domain natively supports passkey enrollment and management routines.
To support passkey discovery, create a public JSON file hosted at /.well-known/passkey-endpoints on your web domain:
JSON
{
"enroll": "https://yourdomain.com/settings/passkeys/add",
"manage": "https://yourdomain.com/settings/passkeys",
"prfUsageDetails": "https://yourdomain.com/docs/passkey-prf"
}
Explanation of the Manifest Keys
enroll: The exact URL where authenticated users can register a new passkey credential.manage: The settings page where users can inspect, rename, or delete their existing registered passkey devices.prfUsageDetails: Optional URL pointing to documentation regarding Pseudo-Random Function (PRF) extension capabilities for local encryption keys.
Current Passkey Adoption in New Zealand and Globally
Passkeys are rapidly rolling out across major platforms, but adoption varies across regional markets. Here is an overview of local and international service availability:
| Platform / Website | Region / Scope | Passkey Support Level | Supported Authentication Methods |
| Global Tech Platforms (Google, Apple, Microsoft, Amazon, PayPal, Revolut) | Global (including NZ) | Full Native Support | Biometrics (Face ID/Touch ID/Windows Hello), Security Keys |
| Treads.NZ | New Zealand | Full Native Support | Biometrics (Touch ID, Face ID), Device PIN |
| Air New Zealand (Airpoints / Account Security) | New Zealand | Full Native Support | Primary login & Multi-Factor Authentication (MFA) via fingerprint, face scan, or PIN |
| adidas NZ | New Zealand | Full Native Support | Chrome, Safari, and Edge on compatible biometric/screen lock devices |
| Trade Me & Major NZ News Portals | New Zealand | Not Yet Supported | Traditional Passwords / Standard 2FA |
While major global networks and forward-thinking platforms like Treads.NZ have adopted WebAuthn, many traditional domestic platforms in New Zealand still rely on legacy passwords.
Step-by-Step Configuration Guide
Follow these steps to configure your database and implement passkey registration and login flows in PHP and JavaScript.
Step 1: Create the Passkeys Database Table
Define a SQL schema to link passkey credentials to existing user accounts.
SQL
CREATE TABLE passkeys (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
credential_id VARCHAR(255) NOT NULL UNIQUE,
public_key TEXT NOT NULL,
sign_count INT UNSIGNED NOT NULL DEFAULT 0,
device_name VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
Step 2: Implement Passkey Registration (Adding a Passkey)
When an authenticated user requests to register a passkey, the server creates a cryptographic challenge. The frontend passes this challenge to navigator.credentials.create().
JavaScript
// JavaScript: Initiate passkey creation request
async function registerPasskey() {
// Fetch challenge from backend PHP endpoint
const response = await fetch('/api/passkeys/register-challenge.php');
const options = await response.json();
// Convert base64 challenge to ArrayBuffer
options.publicKey.challenge = Uint8Array.from(atob(options.publicKey.challenge), c => c.charCodeAt(0));
options.publicKey.user.id = Uint8Array.from(atob(options.publicKey.user.id), c => c.charCodeAt(0));
// Invoke browser WebAuthn API
const credential = await navigator.credentials.create({ publicKey: options.publicKey });
// Send the generated public key back to the PHP backend
await fetch('/api/passkeys/register-verify.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: credential.id,
rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
response: {
attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.attestationObject))),
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON)))
}
})
});
}
Step 3: Implement Passkey Authentication (Login Flow)
During login, the browser calls navigator.credentials.get() to sign a challenge using the stored private key.
JavaScript
// JavaScript: Passkey authentication call
async function loginWithPasskey() {
const response = await fetch('/api/passkeys/login-challenge.php');
const options = await response.json();
options.publicKey.challenge = Uint8Array.from(atob(options.publicKey.challenge), c => c.charCodeAt(0));
const assertion = await navigator.credentials.get({ publicKey: options.publicKey });
const verifyResponse = await fetch('/api/passkeys/login-verify.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: assertion.id,
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(assertion.response.clientDataJSON))),
authenticatorData: btoa(String.fromCharCode(...new Uint8Array(assertion.response.authenticatorData))),
signature: btoa(String.fromCharCode(...new Uint8Array(assertion.response.signature)))
})
});
if (verifyResponse.ok) {
window.location.href = '/dashboard';
}
}
Frequently Asked Questions (FAQ)
What happens if a user loses their device or passkey?
Because passkey private keys are tied to the physical device or account vault (such as iCloud Keychain or Google Password Manager), losing an un-synced device means the credential cannot be used. It is essential to maintain backup recovery paths such as email magic links, single-use recovery codes, or secondary passkey registrations.
Are passkeys vulnerable to phishing attacks?
No. Passkeys are intrinsically bound to the specific domain origin (e.g., treads.nz or airnewzealand.co.nz) in the browser. Even if a user is tricked into visiting a fraudulent website, the browser will refuse to supply the signed challenge payload to an untrusted domain origin.
Conclusion
Integrating WebAuthn passkeys drastically improves user login speed while eliminating common password-based security vulnerabilities. By publishing a standard .well-known/passkey-endpoints file, defining a proper SQL database schema, and binding JavaScript client calls to a secure PHP backend, you can provide seamless biometric authentication across all modern browsers and operating systems.
![]()