109 lines · 3.8 KB
Raw Download
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