| 1 |
<?php |
| 2 |
// Manage the passkeys that can sign in to this site. Registration happens in |
| 3 |
// the browser against passkey-api.php; this page is the list and the delete. |
| 4 |
require_once __DIR__ . '/inc/webauthn.php'; |
| 5 |
require_once __DIR__ . '/inc/layout.php'; |
| 6 |
|
| 7 |
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); |
| 8 |
header('Pragma: no-cache'); |
| 9 |
header('Expires: 0'); |
| 10 |
|
| 11 |
cache_buster_redirect(); |
| 12 |
|
| 13 |
require_login(); |
| 14 |
|
| 15 |
if ($_SERVER['REQUEST_METHOD'] === 'POST') { |
| 16 |
check_csrf(); |
| 17 |
if (($_POST['action'] ?? '') === 'delete') { |
| 18 |
$st = db()->prepare('SELECT label FROM passkeys WHERE id = ?'); |
| 19 |
$st->execute([(int)($_POST['id'] ?? 0)]); |
| 20 |
$label = $st->fetchColumn(); |
| 21 |
if ($label !== false) { |
| 22 |
backup_db(); |
| 23 |
db()->prepare('DELETE FROM passkeys WHERE id = ?')->execute([(int)$_POST['id']]); |
| 24 |
flash('Removed the passkey "' . $label . '".'); |
| 25 |
} |
| 26 |
} |
| 27 |
header('Location: passkeys.php'); |
| 28 |
exit; |
| 29 |
} |
| 30 |
|
| 31 |
$rp = webauthn_rp_id(); |
| 32 |
|
| 33 |
// Passkeys are bound to the host they were created on, so one made on |
| 34 |
// localhost cannot sign in to the live domain. Listing the other hosts' |
| 35 |
// keys separately explains why they are not usable here rather than |
| 36 |
// hiding them and looking like they were lost. |
| 37 |
$mine = db()->prepare('SELECT * FROM passkeys WHERE rp_id = ? ORDER BY id DESC'); |
| 38 |
$mine->execute([$rp]); |
| 39 |
$mine = $mine->fetchAll(); |
| 40 |
|
| 41 |
$others = db()->prepare('SELECT rp_id, COUNT(*) n FROM passkeys WHERE rp_id <> ? GROUP BY rp_id ORDER BY rp_id'); |
| 42 |
$others->execute([$rp]); |
| 43 |
$others = $others->fetchAll(); |
| 44 |
|
| 45 |
page_header('Passkeys'); |
| 46 |
?> |
| 47 |
<h1>Passkeys</h1> |
| 48 |
<p class="muted"> |
| 49 |
A passkey signs you in with your device’s fingerprint, face, or screen lock instead of the password. |
| 50 |
The password keeps working — keep it somewhere safe as the way back in if you lose your devices. |
| 51 |
</p> |
| 52 |
|
| 53 |
<?php if (!webauthn_available()): ?> |
| 54 |
<p class="error"> |
| 55 |
Passkeys need a secure connection. This page was loaded over plain HTTP from |
| 56 |
<strong><?= e($rp) ?></strong>, so the browser will not allow them here. Load the site over HTTPS |
| 57 |
(or via <code>localhost</code>) and this page will work. |
| 58 |
</p> |
| 59 |
<?php else: ?> |
| 60 |
<script type="application/json" id="pkCfg"><?= json_encode(['csrf' => csrf_token()], JSON_HEX_TAG | JSON_HEX_AMP) ?></script> |
| 61 |
|
| 62 |
<label for="pkLabel">Name this device (optional)</label> |
| 63 |
<input id="pkLabel" maxlength="60" placeholder="iPhone, work laptop…"> |
| 64 |
<div class="actions"> |
| 65 |
<button type="button" id="pkAdd">Add a passkey</button> |
| 66 |
<a class="btn secondary" href="index.php">Done</a> |
| 67 |
</div> |
| 68 |
<p class="muted" id="pkMsg">Passkeys added here work on <strong><?= e($rp) ?></strong>.</p> |
| 69 |
<?php endif; ?> |
| 70 |
|
| 71 |
<?php if ($mine): ?> |
| 72 |
<h1>Registered</h1> |
| 73 |
<?php foreach ($mine as $k): ?> |
| 74 |
<div class="card row"> |
| 75 |
<div class="grow"> |
| 76 |
<strong><?= e((string)$k['label']) ?></strong> |
| 77 |
<div class="muted"> |
| 78 |
Added <?= e(substr((string)$k['created_at'], 0, 10)) ?> · |
| 79 |
<?= $k['last_used_at'] ? e(rel_time($k['last_used_at'])) : 'never used' ?> |
| 80 |
</div> |
| 81 |
</div> |
| 82 |
<div class="rowlinks"> |
| 83 |
<form method="post" onsubmit="return confirm('Remove this passkey? That device will no longer be able to sign in.')"> |
| 84 |
<?= csrf_field() ?> |
| 85 |
<input type="hidden" name="action" value="delete"> |
| 86 |
<input type="hidden" name="id" value="<?= (int)$k['id'] ?>"> |
| 87 |
<button class="danger btn-sm">Remove</button> |
| 88 |
</form> |
| 89 |
</div> |
| 90 |
</div> |
| 91 |
<?php endforeach; ?> |
| 92 |
<?php else: ?> |
| 93 |
<p class="muted">No passkeys registered for <?= e($rp) ?> yet.</p> |
| 94 |
<?php endif; ?> |
| 95 |
|
| 96 |
<?php if ($others): ?> |
| 97 |
<p class="muted"> |
| 98 |
You also have passkeys registered for |
| 99 |
<?php $bits = []; |
| 100 |
foreach ($others as $o) { |
| 101 |
$bits[] = $o['n'] . ' on ' . $o['rp_id']; |
| 102 |
} |
| 103 |
echo e(implode(', ', $bits)); ?>. |
| 104 |
Those only work when you visit the site at that address. |
| 105 |
</p> |
| 106 |
<?php endif; ?> |
| 107 |
<?php |
| 108 |
page_footer(['assets/passkey.js']); |
| 109 |
|