PHP

How to Implement Passkey Authentication on Your PHP Website

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 gmp or openssl extension 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/WebAuthn or web-token/jwt-framework installed 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 / WebsiteRegion / ScopePasskey Support LevelSupported Authentication Methods
Global Tech Platforms (Google, Apple, Microsoft, Amazon, PayPal, Revolut)Global (including NZ)Full Native SupportBiometrics (Face ID/Touch ID/Windows Hello), Security Keys
Treads.NZNew ZealandFull Native SupportBiometrics (Touch ID, Face ID), Device PIN
Air New Zealand (Airpoints / Account Security)New ZealandFull Native SupportPrimary login & Multi-Factor Authentication (MFA) via fingerprint, face scan, or PIN
adidas NZ
New ZealandFull Native SupportChrome, Safari, and Edge on compatible biometric/screen lock devices
Trade Me & Major NZ News Portals
New ZealandNot Yet SupportedTraditional 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.

Loading