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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software: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 · 7h 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

server/web/templates/account.html +17−2

@@ -80,19 +80,34 @@
80 80 {{if .Credentials}}
81 81 <div class="panel">
82 82 <table>
83 <thead><tr><th>Credential</th><th>Label</th><th>Created</th><th>Last used</th></tr></thead>
83 <thead><tr><th>Credential</th><th>Label</th><th>Created</th><th>Last used</th><th></th></tr></thead>
84 84 <tbody>
85 85 {{range .Credentials}}
86 86 <tr>
87 87 <td class="sha">{{credid .ID}}</td>
88 <td>{{if .Label}}{{.Label}}{{else}}passkey{{end}}</td>
88 <td>
89 <form method="post" action="/-/account/passkeys/rename" class="inline">
90 <input type="hidden" name="csrf" value="{{$.CSRF}}">
91 <input type="hidden" name="credential" value="{{credid .ID}}">
92 <input type="text" name="label" value="{{.Label}}" maxlength="128" placeholder="passkey" aria-label="Passkey label">
93 <button class="link" type="submit">rename</button>
94 </form>
95 </td>
89 96 <td class="muted">{{since .Created.Unix}}</td>
90 97 <td class="muted">{{if .LastUsed.IsZero}}never{{else}}{{since .LastUsed.Unix}}{{end}}</td>
98 <td>
99 <form method="post" action="/-/account/passkeys/remove" data-confirm="Remove this passkey? Sessions that signed in with it end now.">
100 <input type="hidden" name="csrf" value="{{$.CSRF}}">
101 <input type="hidden" name="credential" value="{{credid .ID}}">
102 <button class="link" type="submit">remove</button>
103 </form>
104 </td>
91 105 </tr>
92 106 {{end}}
93 107 </tbody>
94 108 </table>
95 109 </div>
110 <p class="muted">A label is only what you call a passkey; renaming one does not change what it can do. Removing one ends the sessions that signed in with it. Your last passkey cannot be removed while it is your only way into your account: register another passkey or add an SSH key first.</p>
96 111 {{else}}
97 112 <div class="empty-state"><p>No passkeys yet.</p></div>
98 113 {{end}}