Skip to content

Instantly share code, notes, and snippets.

@nst
Last active August 23, 2025 18:00
Show Gist options
  • Save nst/f50b4774827c27c4de749742ef0ea046 to your computer and use it in GitHub Desktop.
Save nst/f50b4774827c27c4de749742ef0ea046 to your computer and use it in GitHub Desktop.
PassKeys demo
<?php
session_start();
$users_file = 'users.json';
$pending_file = 'pending.json';
function loadData($file) {
if (!file_exists($file)) {
return [];
}
$content = file_get_contents($file);
$data = json_decode($content, true);
if (!is_array($data)) {
return [];
}
return $data;
}
function loadUsers($file) {
$data = loadData($file);
// Clean up and restore binary data in public keys for users file only
$cleanData = [];
foreach ($data as $username => $userData) {
if (is_array($userData) && isset($userData['publicKey'])) {
$cleanData[$username] = $userData;
$cleanData[$username]['publicKey'] = restorePublicKey($userData['publicKey']);
}
}
return $cleanData;
}
function saveUsers($file, $users) {
// Prepare all users' public keys for storage
$preparedUsers = [];
foreach ($users as $username => $userData) {
if (is_array($userData)) {
$preparedUsers[$username] = $userData;
if (isset($userData['publicKey'])) {
$preparedUsers[$username]['publicKey'] = preparePublicKey($userData['publicKey']);
}
}
}
return saveData($file, $preparedUsers);
}
function saveData($file, $data) {
$json = json_encode($data, JSON_PRETTY_PRINT);
return $json !== false && file_put_contents($file, $json) !== false;
}
function preparePublicKey($publicKey) {
if (!is_array($publicKey)) return $publicKey;
$prepared = [];
foreach ($publicKey as $key => $value) {
if (is_string($value) && !mb_check_encoding($value, 'UTF-8')) {
$prepared[$key] = ['_binary' => true, 'data' => base64_encode($value)];
} else {
$prepared[$key] = $value;
}
}
return $prepared;
}
function restorePublicKey($publicKey) {
if (!is_array($publicKey)) return $publicKey;
$restored = [];
foreach ($publicKey as $key => $value) {
if (is_array($value) && isset($value['_binary']) && $value['_binary'] === true) {
$decoded = base64_decode($value['data']);
if ($decoded === false) {
error_log("Failed to decode base64 data for key $key");
$restored[$key] = $value; // Keep original if decode fails
} else {
$restored[$key] = $decoded;
}
} else {
$restored[$key] = $value;
}
}
return $restored;
}
function b64encode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function b64decode($data) {
return base64_decode(strtr($data, '-_', '+/') . str_repeat('=', 3 - (3 + strlen($data)) % 4));
}
function randomBytes($length) {
try {
return random_bytes($length);
} catch (Exception $e) {
// Fallback for systems where random_bytes might fail (e.g., old PHP versions or missing /dev/urandom)
// This is not cryptographically secure and should only be used as a last resort in a demo.
// For production, ensure random_bytes is available.
error_log("random_bytes failed: " . $e->getMessage() . ". Falling back to insecure random number generation.");
$bytes = '';
for ($i = 0; $i < $length; $i++) {
$bytes .= chr(mt_rand(0, 255));
}
return $bytes;
}
}
function decodeCBOR($data) {
$pos = 0;
return decodeCBORValue($data, $pos);
}
function decodeCBORValue($data, &$pos) {
if ($pos >= strlen($data)) return null;
$byte = ord($data[$pos++]);
$majorType = ($byte >> 5) & 0x07;
$additionalInfo = $byte & 0x1f;
switch ($majorType) {
case 0: return decodeCBORUint($data, $pos, $additionalInfo);
case 1: return -1 - decodeCBORUint($data, $pos, $additionalInfo);
case 2:
case 3:
$length = decodeCBORUint($data, $pos, $additionalInfo);
$result = substr($data, $pos, $length);
$pos += $length;
return $result;
case 4:
$length = decodeCBORUint($data, $pos, $additionalInfo);
$result = [];
for ($i = 0; $i < $length; $i++) {
$result[] = decodeCBORValue($data, $pos);
}
return $result;
case 5:
$length = decodeCBORUint($data, $pos, $additionalInfo);
$result = [];
for ($i = 0; $i < $length; $i++) {
$key = decodeCBORValue($data, $pos);
$value = decodeCBORValue($data, $pos);
$result[$key] = $value;
}
return $result;
case 7:
if ($additionalInfo == 20) return false;
if ($additionalInfo == 21) return true;
if ($additionalInfo == 22) return null;
}
return null;
}
function decodeCBORUint($data, &$pos, $additionalInfo) {
if ($additionalInfo < 24) return $additionalInfo;
if ($additionalInfo == 24) return ord($data[$pos++]);
if ($additionalInfo == 25) {
$result = (ord($data[$pos]) << 8) | ord($data[$pos + 1]);
$pos += 2;
return $result;
}
if ($additionalInfo == 26) {
$result = (ord($data[$pos]) << 24) | (ord($data[$pos + 1]) << 16) |
(ord($data[$pos + 2]) << 8) | ord($data[$pos + 3]);
$pos += 4;
return $result;
}
// Handle 64-bit integers (for very large lengths)
if ($additionalInfo == 27) {
$high = (ord($data[$pos]) << 24) | (ord($data[$pos + 1]) << 16) |
(ord($data[$pos + 2]) << 8) | ord($data[$pos + 3]);
$low = (ord($data[$pos + 4]) << 24) | (ord($data[$pos + 5]) << 16) |
(ord($data[$pos + 6]) << 8) | ord($data[$pos + 7]);
$pos += 8;
// This won't correctly represent full 64-bit integers on 32-bit PHP,
// but for typical WebAuthn data, 32-bit is usually sufficient for lengths.
return ($high << 32) | $low;
}
return 0;
}
function extractPublicKey($attestationObjectB64) {
$attestationObject = decodeCBOR(b64decode($attestationObjectB64));
if (!isset($attestationObject['authData']) || strlen($attestationObject['authData']) < 37) {
return ['error' => 'Invalid attestation data'];
}
$authData = $attestationObject['authData'];
$flags = ord($authData[32]);
if (($flags & 0x40) === 0) { // Check AT flag for attested credential data
return ['error' => 'No credential data (AT flag not set)'];
}
$offset = 37; // Skip RP ID Hash (32 bytes) + Flags (1 byte) + Sign Count (4 bytes)
// AAGUID (16 bytes)
$aaguid = substr($authData, $offset, 16);
$offset += 16;
// Credential ID Length (2 bytes)
$credIdLength = unpack('n', substr($authData, $offset, 2))[1];
$offset += 2;
// Credential ID
$credentialId = substr($authData, $offset, $credIdLength);
$offset += $credIdLength;
// COSE Public Key
$publicKey = decodeCBOR(substr($authData, $offset));
if (!$publicKey) {
return ['error' => 'Failed to decode public key'];
}
return [
'publicKey' => $publicKey,
'credentialId' => b64encode($credentialId),
'aaguid' => bin2hex($aaguid),
'signCount' => unpack('N', substr($authData, 33, 4))[1] // Sign count is at byte 33
];
}
function formatPublicKey($publicKey) {
if (!is_array($publicKey)) return 'Invalid public key format';
$output = "COSE Public Key:\n";
// Key type and algorithm
$keyTypes = [1 => 'OKP', 2 => 'EC2', 3 => 'RSA'];
$algorithms = [-7 => 'ES256', -257 => 'RS256', -8 => 'EdDSA']; // -8 is EdDSA for OKP
if (isset($publicKey[1])) { // kty
$kty = $publicKey[1];
$output .= "• Key Type: $kty (" . ($keyTypes[$kty] ?? 'Unknown') . ")\n";
}
if (isset($publicKey[3])) { // alg
$alg = $publicKey[3];
$output .= "• Algorithm: $alg (" . ($algorithms[$alg] ?? 'Unknown') . ")\n";
}
// EC2 key details (kty = 2)
if (isset($publicKey[1]) && $publicKey[1] == 2) {
$curves = [1 => 'P-256', 2 => 'P-384', 3 => 'P-521'];
if (isset($publicKey[-1])) { // crv
$crv = $publicKey[-1];
$output .= "• Curve: $crv (" . ($curves[$crv] ?? 'Unknown') . ")\n";
}
if (isset($publicKey[-2])) { // x-coordinate
$output .= "• X coordinate: " . bin2hex($publicKey[-2]) . "\n";
}
if (isset($publicKey[-3])) { // y-coordinate
$output .= "• Y coordinate: " . bin2hex($publicKey[-3]) . "\n";
}
}
// OKP key details (kty = 1)
if (isset($publicKey[1]) && $publicKey[1] == 1) {
$curves = [6 => 'Ed25519', 7 => 'X25519']; // Example curves for OKP
if (isset($publicKey[-1])) { // crv
$crv = $publicKey[-1];
$output .= "• Curve: $crv (" . ($curves[$crv] ?? 'Unknown') . ")\n";
}
if (isset($publicKey[-2])) { // x-coordinate (public key for OKP)
$output .= "• Public Key (x): " . bin2hex($publicKey[-2]) . "\n";
}
}
// RSA key details (kty = 3)
if (isset($publicKey[1]) && $publicKey[1] == 3) {
if (isset($publicKey[-1])) { // n (modulus)
$output .= "• Modulus (n): " . bin2hex($publicKey[-1]) . "\n";
}
if (isset($publicKey[-2])) { // e (exponent)
$output .= "• Exponent (e): " . bin2hex($publicKey[-2]) . "\n";
}
}
return $output;
}
function verifySignature($publicKey, $signedData, $signature) {
if (!is_array($publicKey) || !isset($publicKey[1]) || $publicKey[1] != 2) { // Only ES256 (EC2) supported here
error_log("verifySignature: Invalid public key format or type. Only EC2 (type 2) is supported for signature verification.");
return false;
}
if (!isset($publicKey[-2]) || !isset($publicKey[-3])) {
error_log("verifySignature: Missing X or Y coordinates in public key.");
return false;
}
$x = $publicKey[-2];
$y = $publicKey[-3];
// Convert public key to PEM format
$publicKeyPem = createPemFromCoordinates($x, $y);
if ($publicKeyPem === false) {
error_log("verifySignature: Failed to create PEM from coordinates.");
return false;
}
// WebAuthn signatures are DER-encoded. OpenSSL expects a DER-encoded signature for EC.
// The signature provided by WebAuthn is already DER-encoded.
$result = openssl_verify($signedData, $signature, $publicKeyPem, OPENSSL_ALGO_SHA256);
if ($result === -1) {
error_log("OpenSSL error during signature verification: " . openssl_error_string());
}
return $result === 1;
}
function createPemFromCoordinates($x, $y) {
// P-256 (prime256v1) OID: 1.2.840.10045.3.1.7
$oid_ecPublicKey = hex2bin('2A8648CE3D0201'); // 1.2.840.10045.2.1 (ecPublicKey)
$oid_prime256v1 = hex2bin('2A8648CE3D030107'); // 1.2.840.10045.3.1.7 (prime256v1)
// SEQUENCE { SEQUENCE { ecPublicKey, prime256v1 }, BIT STRING }
// BIT STRING is 0x04 || X || Y for uncompressed point
// SubjectPublicKeyInfo ::= SEQUENCE {
// algorithm AlgorithmIdentifier,
// subjectPublicKey BIT STRING }
// AlgorithmIdentifier ::= SEQUENCE {
// algorithm OBJECT IDENTIFIER,
// parameters ANY DEFINED BY algorithm OPTIONAL }
// ECPoint is 04 || X || Y
$ecPoint = "\x04" . $x . $y;
// Build the ASN.1 structure
// AlgorithmIdentifier (SEQUENCE of OIDs)
$algo_sequence = "\x30\x13" . // SEQUENCE (19 bytes)
"\x06\x07" . $oid_ecPublicKey . // OBJECT IDENTIFIER ecPublicKey (7 bytes)
"\x06\x08" . $oid_prime256v1; // OBJECT IDENTIFIER prime256v1 (8 bytes)
// BIT STRING (public key)
// The 0x00 at the beginning indicates 0 unused bits in the last byte
$bit_string = "\x03" . chr(strlen($ecPoint) + 1) . "\x00" . $ecPoint;
// Full SubjectPublicKeyInfo SEQUENCE
$publicKeyDer = "\x30" . chr(strlen($algo_sequence) + strlen($bit_string)) .
$algo_sequence . $bit_string;
return "-----BEGIN PUBLIC KEY-----\n" .
chunk_split(base64_encode($publicKeyDer), 64, "\n") .
"-----END PUBLIC KEY-----\n";
}
$users = loadUsers($users_file);
$pending = loadData($pending_file);
if (!is_array($users)) $users = [];
if (!is_array($pending)) $pending = [];
$action = $_GET['action'] ?? 'home';
$msg = $_SESSION['msg'] ?? '';
unset($_SESSION['msg']);
// Handle AJAX requests for registration/login options
if ($action === 'get_register_options' && $_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$username = $_POST['username'] ?? '';
if (isset($users[$username])) {
echo json_encode(['error' => "Username '$username' already exists"]);
exit;
}
$challenge = randomBytes(32);
$token = bin2hex(randomBytes(16));
$pending[$token] = [
'type' => 'register',
'username' => $username,
'challenge' => b64encode($challenge),
'expires' => time() + 300 // 5 minutes
];
saveData($pending_file, $pending);
$options = [
'challenge' => b64encode($challenge),
'rp' => ['name' => 'Passkeys Demo'],
'user' => [
'id' => b64encode($username),
'name' => $username,
'displayName' => $username
],
'pubKeyCredParams' => [['type' => 'public-key', 'alg' => -7]],
'timeout' => 60000,
'authenticatorSelection' => ['userVerification' => 'preferred'], // <--- REMOVE 'authenticatorAttachment' entirely
'attestation' => 'none'
];
echo json_encode(['options' => $options, 'token' => $token]);
exit;
}
if ($action === 'get_login_options' && $_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$username = $_POST['username'] ?? '';
if (!isset($users[$username])) {
echo json_encode(['error' => "Username '$username' not found"]);
exit;
}
$challenge = randomBytes(32);
$token = bin2hex(randomBytes(16));
$pending[$token] = [
'type' => 'login',
'username' => $username,
'challenge' => b64encode($challenge),
'expires' => time() + 300
];
saveData($pending_file, $pending);
$options = [
'challenge' => b64encode($challenge),
'allowCredentials' => [
['type' => 'public-key', 'id' => $users[$username]['credentialId'], 'transports' => ['internal', 'hybrid']]
], // Specify transports as per WebAuthn Level 2
'timeout' => 60000,
'userVerification' => 'preferred'
];
echo json_encode(['options' => $options, 'token' => $token]);
exit;
}
// Handle AJAX responses for registration completion
if ($action === 'register_finish' && $_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$token = $_POST['token'] ?? '';
if (!isset($pending[$token]) || $pending[$token]['type'] !== 'register' || $pending[$token]['expires'] < time()) {
echo json_encode(['error' => 'Invalid or expired registration request']);
exit;
}
$reg_data = $pending[$token];
$username = $reg_data['username'];
$client_data_json = b64decode($_POST['clientData']);
$client_data = json_decode($client_data_json, true);
if ($client_data['challenge'] !== $reg_data['challenge']) {
echo json_encode(['error' => 'Invalid challenge in clientData']);
exit;
}
if ($client_data['origin'] !== (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']) {
echo json_encode(['error' => 'Invalid origin in clientData: ' . $client_data['origin'] . ' vs ' . ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'])]);
exit;
}
if ($client_data['type'] !== 'webauthn.create') {
echo json_encode(['error' => 'Invalid clientData type']);
exit;
}
$result = extractPublicKey($_POST['attestation']);
if (isset($result['error'])) {
echo json_encode(['error' => 'Failed to extract public key: ' . $result['error']]);
exit;
}
$users[$username] = [
'credentialId' => $_POST['id'],
'publicKey' => $result['publicKey'], // Keep as binary, saveUsers will handle encoding
'aaguid' => $result['aaguid'],
'signCount' => $result['signCount'],
'registeredAt' => date('Y-m-d H:i:s')
];
if (!saveUsers($users_file, $users)) {
$lastError = error_get_last();
$errorMsg = 'Failed to save user data';
if ($lastError) {
$errorMsg .= ': ' . $lastError['message'];
}
echo json_encode(['error' => $errorMsg]);
exit;
}
unset($pending[$token]);
saveData($pending_file, $pending);
$_SESSION['show_public_key'] = $result['publicKey'];
$_SESSION['show_username'] = $username;
echo json_encode(['success' => true]);
exit;
}
// Handle AJAX responses for login completion
if ($action === 'login_finish' && $_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$token = $_POST['token'] ?? '';
if (!isset($pending[$token]) || $pending[$token]['type'] !== 'login' || $pending[$token]['expires'] < time()) {
echo json_encode(['error' => 'Invalid or expired login request']);
exit;
}
$login_data = $pending[$token];
$username = $login_data['username'];
$userData = $users[$username] ?? null;
if (!$userData) {
echo json_encode(['error' => 'User data not found for ' . $username]);
exit;
}
$client_data_json = b64decode($_POST['clientData']);
$client_data = json_decode($client_data_json, true);
if ($client_data['challenge'] !== $login_data['challenge']) {
echo json_encode(['error' => 'Invalid challenge in clientData']);
exit;
}
if ($client_data['origin'] !== (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']) {
echo json_encode(['error' => 'Invalid origin in clientData']);
exit;
}
if ($client_data['type'] !== 'webauthn.get') {
echo json_encode(['error' => 'Invalid clientData type']);
exit;
}
if ($_POST['id'] !== $userData['credentialId']) {
echo json_encode(['error' => 'Credential ID mismatch']);
exit;
}
$authData = b64decode($_POST['authData']);
$clientDataHash = hash('sha256', $client_data_json, true);
$signedData = $authData . $clientDataHash;
$signature = b64decode($_POST['signature']);
// Update signCount from authData
$newSignCount = unpack('N', substr($authData, 33, 4))[1];
if ($newSignCount < $userData['signCount']) {
// This indicates a potential replay attack or credential cloning.
// For a simple demo, we just log and warn. In production, this should trigger stronger security measures.
error_log("Sign count for user {$username} decreased from {$userData['signCount']} to {$newSignCount}. Possible replay attack!");
// Optionally, you might want to fail the login here:
// echo json_encode(['error' => 'Sign count anomaly detected.']); exit;
}
$users[$username]['signCount'] = $newSignCount;
saveUsers($users_file, $users); // Save updated signCount
if (!verifySignature($userData['publicKey'], $signedData, $signature)) {
echo json_encode(['error' => 'Signature verification failed']);
exit;
}
$_SESSION['user'] = $username;
unset($pending[$token]);
saveData($pending_file, $pending);
echo json_encode(['success' => true, 'message' => "Welcome $username! Signature verified."]);
exit;
}
if ($action === 'logout') {
unset($_SESSION['user']);
$_SESSION['msg'] = 'Logged out';
header('Location: ?');
exit;
}
if ($action === 'delete_credential' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
if (!isset($users[$username])) {
$_SESSION['msg'] = 'User not found.';
header('Location: ?');
exit;
}
// In a real application, you might want to challenge the user (e.g., re-authenticate)
// before allowing credential deletion. For this demo, direct deletion.
// Remove the public key and credential ID for this user
unset($users[$username]['credentialId']);
unset($users[$username]['publicKey']);
unset($users[$username]['aaguid']);
unset($users[$username]['signCount']);
// You might choose to delete the entire user entry if they have no other credentials
// For this demo, we'll keep the user entry but mark them as un-registered for passkey.
if (saveUsers($users_file, $users)) {
$_SESSION['msg'] = "Passkey for user '$username' removed successfully. User still exists but needs to register a new passkey to login.";
} else {
$_SESSION['msg'] = 'Failed to remove passkey.';
}
header('Location: ?');
exit;
}
if ($action === 'reset_users') {
if (saveUsers($users_file, [])) {
$_SESSION['msg'] = 'All users deleted successfully';
} else {
$_SESSION['msg'] = 'Failed to reset users database';
}
header('Location: ?');
exit;
}
// Clean up expired tokens
foreach ($pending as $token => $data) {
if ($data['expires'] < time()) {
unset($pending[$token]);
}
}
saveData($pending_file, $pending);
?>
<!DOCTYPE html>
<html>
<head>
<title>Passkeys Demo</title>
</head>
<body>
<h1>Passkeys Demo</h1>
<?php if (isset($_SESSION['user'])): ?>
<p>Logged in as: <strong><?= htmlspecialchars($_SESSION['user']) ?></strong>
<a href="?action=logout">Logout</a></p>
<?php else: ?>
<h2>Register / Login</h2>
<p>Enter your username to either register a new passkey or login with an existing one.</p>
<input type="text" id="username" placeholder="Username" required>
<button onclick="handlePasskeyAction('register')">Register Passkey</button>
<button onclick="handlePasskeyAction('login')">Login with Passkey</button>
<?php endif; ?>
<?php if ($msg): ?>
<p><strong><?= htmlspecialchars($msg) ?></strong></p>
<?php endif; ?>
<div id="status" style="display:none;"></div>
<?php if (isset($_SESSION['show_public_key'])): ?>
<h3>Registration Successful for <?= htmlspecialchars($_SESSION['show_username']) ?>!</h3>
<h4>Extracted Public Key:</h4>
<pre><?= htmlspecialchars(formatPublicKey($_SESSION['show_public_key'])) ?></pre>
<?php unset($_SESSION['show_public_key'], $_SESSION['show_username']); ?>
<?php endif; ?>
<h3>Registered Users (<?= count($users) ?>):</h3>
<?php if (empty($users)): ?>
<p><em>No users registered yet.</em></p>
<?php else: ?>
<ul>
<?php foreach ($users as $username => $userData): ?>
<?php if (is_array($userData) && isset($userData['credentialId'])): ?>
<li>
<strong><?= htmlspecialchars($username) ?></strong>
<button onclick="deleteCredential('<?= htmlspecialchars($username) ?>')">Remove Passkey</button>
<br>
Registered: <?= htmlspecialchars($userData['registeredAt'] ?? 'Unknown') ?><br>
AAGUID: <?= htmlspecialchars($userData['aaguid'] ?? 'Unknown') ?><br>
Credential ID: <?= htmlspecialchars($userData['credentialId'] ?? 'N/A') ?><br>
Sign Count: <?= htmlspecialchars($userData['signCount'] ?? 'N/A') ?><br>
<strong>Public Key:</strong>
<pre><?= htmlspecialchars(formatPublicKey($userData['publicKey'])) ?></pre>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<button onclick="if(confirm('Delete ALL users?')) location.href='?action=reset_users'">Reset All Users</button>
<hr>
<p>Code: <a href="https://gist.github.com/nst/f50b4774827c27c4de749742ef0ea046">https://gist.github.com/nst/f50b4774827c27c4de749742ef0ea046</a></p>
<p>Protocol and security smells: <a href="https://seriot.ch/pk/202508_passkeys.pdf">202508_passkeys.pdf</a></p>
<script>
function b64decode(s) {
return Uint8Array.from(atob(s.replace(/-/g,'+').replace(/_/g,'/')), c => c.charCodeAt(0));
}
function b64encode(buf) {
return btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
}
function togglePublicKey(username) {
const keyDiv = document.getElementById('key-' + username);
if (keyDiv.style.display === 'none') {
keyDiv.style.display = 'block';
} else {
keyDiv.style.display = 'none';
}
}
async function showStatus(message, isError = false, append = false) {
const statusDiv = document.getElementById('status');
if (append) {
statusDiv.innerHTML += message;
} else {
statusDiv.innerHTML = message;
}
statusDiv.style.display = 'block';
}
async function handlePasskeyAction(type) {
const username = document.getElementById('username').value;
if (!username) {
showStatus('Please enter a username.', true);
return;
}
showStatus('Preparing passkey ' + type + '...');
try {
const response = await fetch(`?action=get_${type}_options`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `username=${encodeURIComponent(username)}`
});
const data = await response.json();
if (data.error) {
showStatus('Error: ' + data.error, true);
return;
}
const options = data.options;
const token = data.token;
showStatus('WebAuthn ' + type + ' options generated:\n<pre>' + JSON.stringify(options, null, 2) + '</pre>', false, false);
await new Promise(resolve => setTimeout(resolve, 1000));
showStatus('<br>Prompting passkey ' + type + '...', false, true);
if (type === 'register') {
await createPasskey(options, token);
} else {
await usePasskey(options, token);
}
} catch (e) {
showStatus('Network or server error: ' + e.message, true);
}
}
async function createPasskey(options, token) {
try {
options.challenge = b64decode(options.challenge);
options.user.id = b64decode(options.user.id);
const cred = await navigator.credentials.create({publicKey: options});
const response = await fetch('?action=register_finish', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
token: token,
id: cred.id,
attestation: b64encode(cred.response.attestationObject),
clientData: b64encode(cred.response.clientDataJSON)
})
});
const data = await response.json();
if (data.success) {
window.location.href = '?'; // Reload to show success message and updated user list
} else {
showStatus('Registration failed: ' + data.error, true);
}
} catch(e) {
showStatus('Passkey creation error: ' + e.message, true);
}
}
async function usePasskey(options, token) {
try {
options.challenge = b64decode(options.challenge);
// The credential ID for allowCredentials must be a Uint8Array
if (options.allowCredentials && options.allowCredentials.length > 0) {
options.allowCredentials[0].id = b64decode(options.allowCredentials[0].id);
} else {
// For login, if no specific credential ID is allowed,
// the browser will allow discovery. This is usually preferred.
delete options.allowCredentials; // Remove if empty or not needed
}
const cred = await navigator.credentials.get({publicKey: options});
const response = await fetch('?action=login_finish', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
token: token,
id: cred.id,
authData: b64encode(cred.response.authenticatorData),
clientData: b64encode(cred.response.clientDataJSON),
signature: b64encode(cred.response.signature)
})
});
const data = await response.json();
if (data.success) {
window.location.href = '?'; // Reload to show success message and login status
} else {
showStatus('Login failed: ' + data.error, true);
}
} catch(e) {
showStatus('Passkey login error: ' + e.message, true);
}
}
function deleteCredential(username) {
if (confirm(`Are you sure you want to remove the passkey for user '${username}'? They will no longer be able to log in with a passkey unless they register a new one.`)) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '?action=delete_credential';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'username';
input.value = username;
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment