Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Let people rename and remove their own passkeys

7d7718fac3edfc9b8e27d1e55721a6f9ca9106af · Keenan Nemetz · 2h ago · parent 5f6163bb

Let people rename and remove their own passkeys

The Account page listed and added passkeys and could do nothing else: a lost or
shared device, or a label typo, needed an instance admin and the CLI. Both are the
person's own credential, so both belong on the page.

- Rename: `identity.RenameCredential` writes the label in one transaction and audits
  `identity.credential-rename` with the passkey and its new label. A label is display
  text, so the credential id and key are untouched and the cap is the one the
  ceremony already uses (`MaxCredentialLabel`). An empty label is allowed and the
  page calls it a passkey.
- Remove: `identity.RemoveCredential` now deletes the sessions of that passkey in the
  same transaction. The foreign key cascades already, and saying it out loud is what
  makes it the intent rather than a schema accident: the session the person is
  reading the page from ends with the passkey it signed in with.
- The last passkey is refused, with an explanation, while the account has no SSH key
  either. With one it is a way in for Git, so an account that has one loses only its
  browser sign-in; with neither, the only way back is an admin invite, and losing
  that should never be a click that silently succeeded. Two passkeys are enough to
  rotate: the last one is protected, not every one.
- The page gets a rename form and a remove form per passkey, each with the CSRF token
  and a confirmation on the removal, and a line saying what removing one does. The
  handlers are scoped by the session account, so the form cannot name somebody else's
  credential, and an unknown one is a 404 rather than a silent no-op.

Docs: `docs/accounts-and-access.md` covers renaming, the last-passkey rule, and the
audit actions for both.

Tests: the label changes and the credential does not, an empty label clears it, an
over-long one is refused where it is accepted, and another account's passkey is not
renameable; a removed passkey takes its sessions with it (identity); and end to end
over the account page — rename one of two, remove one of several, a post without the
token is refused, and the last passkey is refused until an SSH key is added.

Closes 01M6SASJYFVH6Z3P6BXTEMA83

docs/accounts-and-access.md +15−3

@@ -149,8 +149,13 @@ WebAuthn ceremony without a canonical secure origin.
149 149 2. The person registers a passkey.
150 150 3. The passkey creates a browser session. The session cookie is HttpOnly and
151 151 Secure; Vole stores only its hash.
152 4. From **Account**, the person can register another passkey, add or remove SSH
153 keys, or review their sessions.
152 4. From **Account**, the person can register another passkey, rename or remove the
153 ones they have, add or remove SSH keys, or review their sessions. A label is
154 display text only: the credential is what signs in, so renaming one is not a
155 re-registration. Removing a passkey cannot be undone, and the last one is
156 refused while it is the account's only way in — an SSH key counts as a way in
157 for Git, so an account with one loses only its browser sign-in. Otherwise the
158 person would need an admin to invite them back.
154 159
155 160 The account field opts into WebAuthn conditional UI (`autocomplete="username
156 161 webauthn"`), so a supporting browser can offer a saved passkey while the field is
@@ -163,7 +168,14 @@ account it belongs to.
163 168 A link is single-use and expires. Issuing a new link invalidates the previous
164 169 one. If the person loses every passkey, an instance admin issues a new link; there
165 170 is no password reset to phish. Retiring an account deletes its sessions
166 immediately, and removing a passkey deletes the sessions that passkey created.
171 immediately, and removing a passkey deletes the sessions that passkey created,
172 including the one the removal was made from.
173
174 Both writes are audited: `identity.credential-rename` names the passkey and its new
175 label, and `identity.credential-remove` names the passkey, with the account as the
176 subject. The account page's own record of the write is `web.passkey.rename` and
177 `web.passkey.remove`, the same way `web.key.add` and `web.key.remove` sit beside the
178 identity actions for SSH keys.
167 179
168 180 The link carries its token in the URL fragment — `https://forge.example.com/-/invite#<token>`
169 181 — so the browser never sends it to the server, a proxy's access log, or the