如何为现有网站注册登录功能实现FIDO合规(PHP版)
Hey there! I’ve implemented FIDO2 (including U2F backwards compatibility) on several PHP-based auth systems, so I’ll walk you through a practical, step-by-step guide that aligns with FIDO compliance standards. Let’s break this down into actionable parts.
Prerequisites
First, make sure you have these in place:
- PHP 7.4 or higher (8.x is recommended for better security and performance)
- Composer for dependency management
- A database (MySQL/MariaDB, PostgreSQL, etc.) with existing user tables
- HTTPS enabled on your site (FIDO2 requires secure contexts—no plain HTTP!)
We’ll use the web-auth/fido2-lib library, which is fully compliant with W3C WebAuthn and FIDO2 specs. Install it via Composer:
composer require web-auth/fido2-lib
Database Setup
You’ll need to store FIDO2 credentials for users. Create a separate table (better than cluttering your existing user table) to handle multiple credentials per user:
CREATE TABLE user_fido_credentials ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, credential_id VARCHAR(255) NOT NULL UNIQUE, public_key TEXT NOT NULL, user_handle VARCHAR(255), transports VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES your_users_table(id) ON DELETE CASCADE );
(Replace your_users_table with your actual user table name)
Step 1: FIDO2 Registration Flow
This lets users register their security key (like YubiKey) alongside their existing account.
1.1 Generate Registration Options (Backend)
First, generate the options that the frontend will use to prompt the user for their security key. Store these options in the session—you’ll need them to verify the response later.
<?php session_start(); require __DIR__ . '/vendor/autoload.php'; use WebAuthn\PublicKeyCredentialCreationOptions; use WebAuthn\PublicKeyCredentialDescriptor; use WebAuthn\WebAuthnManager; use WebAuthn\TrustPath\EmptyTrustPath; use WebAuthn\MetadataService\NullMetadataService; // Initialize FIDO2 manager $webAuthnManager = new WebAuthnManager( 'Your Website Name', // RP (Relying Party) name 'https://your-domain.com', // RP ID (must match your site's domain) new NullMetadataService(), new EmptyTrustPath() ); // Get the logged-in user (adjust this to your existing auth system) $user = getLoggedInUser(); // Replace with your user fetch logic // Generate registration options $options = $webAuthnManager->createPublicKeyCredentialCreationOptions( $user->id, // User ID (unique for your user) $user->email, // User display name $user->email // User name ); // Store options in session for later verification $_SESSION['fido2_registration_options'] = $options; // Return options as JSON to frontend header('Content-Type: application/json'); echo json_encode($options); ?>
1.2 Handle Frontend Registration
On the frontend, use the WebAuthn API to prompt the user for their security key. Here’s a vanilla JS example:
async function registerFido2() { // Fetch registration options from backend const response = await fetch('/generate-registration-options'); const options = await response.json(); // Convert options to WebAuthn-compatible format options.user.id = Uint8Array.from(atob(options.user.id), c => c.charCodeAt(0)); options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0)); // Prompt user for security key const credential = await navigator.credentials.create({ publicKey: options }); // Convert credential to JSON-serializable format const credentialData = { id: credential.id, rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))), type: credential.type, response: { attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.attestationObject))), clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))) } }; // Send credential to backend for verification await fetch('/verify-registration', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentialData) }); }
1.3 Verify Registration Response (Backend)
Now validate the credential sent from the frontend and store it in your database.
<?php session_start(); require __DIR__ . '/vendor/autoload.php'; use WebAuthn\WebAuthnManager; use WebAuthn\TrustPath\EmptyTrustPath; use WebAuthn\MetadataService\NullMetadataService; // Initialize manager (same as before) $webAuthnManager = new WebAuthnManager( 'Your Website Name', 'https://your-domain.com', new NullMetadataService(), new EmptyTrustPath() ); // Get registration options from session $options = $_SESSION['fido2_registration_options']; unset($_SESSION['fido2_registration_options']); // Clean up session // Get frontend data $credentialData = json_decode(file_get_contents('php://input'), true); try { // Verify the credential $publicKeyCredential = $webAuthnManager->loadAndVerifyPublicKeyCredential( $options, $credentialData, $_SERVER['REMOTE_ADDR'] // User's IP address ); // Extract credential details to store $credentialId = base64_encode($publicKeyCredential->getRawId()); $publicKey = $publicKeyCredential->getPublicKey()->asPem(); $userHandle = base64_encode($publicKeyCredential->getUserHandle()); $transports = implode(',', $publicKeyCredential->getTransports() ?? []); // Store in database (adjust to your DB layer) $pdo = new PDO('mysql:host=localhost;dbname=your_db', 'user', 'pass'); $stmt = $pdo->prepare("INSERT INTO user_fido_credentials (user_id, credential_id, public_key, user_handle, transports) VALUES (?, ?, ?, ?, ?)"); $stmt->execute([ getLoggedInUser()->id, // Replace with your user ID $credentialId, $publicKey, $userHandle, $transports ]); // Return success response header('Content-Type: application/json'); echo json_encode(['success' => true]); } catch (\Exception $e) { // Handle verification errors header('Content-Type: application/json', true, 400); echo json_encode(['success' => false, 'error' => $e->getMessage()]); } ?>
Step 2: FIDO2 Authentication Flow
This lets users log in using their registered security key instead of (or alongside) a password.
2.1 Generate Authentication Options (Backend)
Generate options that prompt the user to present their security key.
<?php session_start(); require __DIR__ . '/vendor/autoload.php'; use WebAuthn\PublicKeyCredentialRequestOptions; use WebAuthn\PublicKeyCredentialDescriptor; use WebAuthn\WebAuthnManager; use WebAuthn\TrustPath\EmptyTrustPath; use WebAuthn\MetadataService\NullMetadataService; $webAuthnManager = new WebAuthnManager( 'Your Website Name', 'https://your-domain.com', new NullMetadataService(), new EmptyTrustPath() ); // Get user from email/username (adjust to your login form logic) $user = getUserByEmail($_POST['email']); // Replace with your user fetch logic // Fetch user's registered FIDO2 credentials $pdo = new PDO('mysql:host=localhost;dbname=your_db', 'user', 'pass'); $stmt = $pdo->prepare("SELECT credential_id FROM user_fido_credentials WHERE user_id = ?"); $stmt->execute([$user->id]); $credentials = $stmt->fetchAll(PDO::FETCH_COLUMN); // Convert credential IDs to descriptors $descriptors = array_map(function($credId) { return new PublicKeyCredentialDescriptor( PublicKeyCredentialDescriptor::TYPE_PUBLIC_KEY, base64_decode($credId) ); }, $credentials); // Generate authentication options $options = $webAuthnManager->createPublicKeyCredentialRequestOptions($descriptors); // Store options in session $_SESSION['fido2_authentication_options'] = $options; $_SESSION['fido2_authentication_user_id'] = $user->id; // Return options as JSON header('Content-Type: application/json'); echo json_encode($options); ?>
2.2 Handle Frontend Authentication
Similar to registration, use the WebAuthn API to get the authentication assertion.
async function authenticateFido2(email) { // Fetch authentication options const response = await fetch('/generate-authentication-options', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email }) }); const options = await response.json(); // Convert options to WebAuthn format options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0)); options.allowCredentials = options.allowCredentials.map(cred => ({ ...cred, id: Uint8Array.from(atob(cred.id), c => c.charCodeAt(0)) })); // Prompt user for security key const assertion = await navigator.credentials.get({ publicKey: options }); // Convert assertion to serializable format const assertionData = { id: assertion.id, rawId: btoa(String.fromCharCode(...new Uint8Array(assertion.rawId))), type: assertion.type, response: { authenticatorData: btoa(String.fromCharCode(...new Uint8Array(assertion.response.authenticatorData))), clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(assertion.response.clientDataJSON))), signature: btoa(String.fromCharCode(...new Uint8Array(assertion.response.signature))), userHandle: assertion.response.userHandle ? btoa(String.fromCharCode(...new Uint8Array(assertion.response.userHandle))) : null } }; // Send to backend for verification const authResponse = await fetch('/verify-authentication', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(assertionData) }); const result = await authResponse.json(); if (result.success) { // Log user in (redirect to dashboard, set session, etc.) window.location.href = '/dashboard'; } }
2.3 Verify Authentication Response (Backend)
Validate the assertion and log the user in.
<?php session_start(); require __DIR__ . '/vendor/autoload.php'; use WebAuthn\WebAuthnManager; use WebAuthn\TrustPath\EmptyTrustPath; use WebAuthn\MetadataService\NullMetadataService; $webAuthnManager = new WebAuthnManager( 'Your Website Name', 'https://your-domain.com', new NullMetadataService(), new EmptyTrustPath() ); // Get session data $options = $_SESSION['fido2_authentication_options']; $userId = $_SESSION['fido2_authentication_user_id']; unset($_SESSION['fido2_authentication_options'], $_SESSION['fido2_authentication_user_id']); // Get frontend assertion data $assertionData = json_decode(file_get_contents('php://input'), true); try { // Fetch user's public key from database $pdo = new PDO('mysql:host=localhost;dbname=your_db', 'user', 'pass'); $stmt = $pdo->prepare("SELECT public_key FROM user_fido_credentials WHERE user_id = ? AND credential_id = ?"); $stmt->execute([$userId, base64_encode(base64_decode($assertionData['rawId']))]); $publicKeyPem = $stmt->fetchColumn(); if (!$publicKeyPem) { throw new Exception('Credential not found'); } // Verify the assertion $webAuthnManager->verifyPublicKeyCredentialRequest( $options, $assertionData, $publicKeyPem, $_SERVER['REMOTE_ADDR'] ); // Log user in (adjust to your existing auth system) $_SESSION['user_id'] = $userId; // Return success header('Content-Type: application/json'); echo json_encode(['success' => true]); } catch (\Exception $e) { header('Content-Type: application/json', true, 401); echo json_encode(['success' => false, 'error' => $e->getMessage()]); } ?>
FIDO Compliance & Security Tips
- Use HTTPS exclusively: FIDO2 won’t work on plain HTTP—browsers enforce this for security.
- Metadata Service: For better compliance, replace
NullMetadataServicewith a real metadata service (like from FIDO Alliance) to validate authenticator metadata. - Encrypt stored credentials: Store
public_keyandcredential_idin encrypted form (use PHP’sopenssl_encryptor a dedicated encryption library) for extra security. - Support multiple credentials: Users might have multiple security keys, so the
user_fido_credentialstable ensures you can store all of them. - Error handling: Avoid exposing detailed error messages to users—return generic messages like "Authentication failed" instead of revealing specific issues.
内容的提问来源于stack exchange,提问作者Khalid Bin Noor

