You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为现有网站注册登录功能实现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.

PHP FIDO2 Implementation Guide for Existing Login/Registration Systems

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 NullMetadataService with a real metadata service (like from FIDO Alliance) to validate authenticator metadata.
  • Encrypt stored credentials: Store public_key and credential_id in encrypted form (use PHP’s openssl_encrypt or a dedicated encryption library) for extra security.
  • Support multiple credentials: Users might have multiple security keys, so the user_fido_credentials table 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:06:15