Skip to content

Commit c8a4226

Browse files
docs: add Account Management page with API endpoints (#23) (#24)
New features/account-management.md documenting the self-service user-rights trio plus passkeys, each with method, path, auth, request body, response shape, and failure modes: - PUT /idp/credentials change password (#351) - GET /idp/account/export pod backup/export, tar.gz + manifest (#353) - DELETE /idp/account account deletion, optional purgeData (#352) - GET/POST /idp/account/delete browser delete flow - POST /idp/passkey/* WebAuthn register/login - jss account delete <user> operator CLI Wired into the Features sidebar after Authentication, added to the features overview table, and cross-linked from authentication.md. Closes #23
1 parent 41c301b commit c8a4226

4 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
---
2+
sidebar_position: 6
3+
title: Account Management
4+
description: Self-service change password, pod backup/export, and account deletion
5+
---
6+
7+
# Account Management
8+
9+
Once an account exists (see [Authentication](./authentication.md) for how to create one
10+
and log in), JSS gives the account owner a self-service "user-rights trio" plus passkey
11+
enrolment. Every action below is scoped to the **authenticated caller's own WebID** — there
12+
is no target parameter, so cross-account access is structurally impossible.
13+
14+
All endpoints accept any of the server's auth schemes: `Authorization: Bearer <token>`,
15+
DPoP-bound tokens, or Nostr NIP-98 signatures.
16+
17+
## Endpoint reference
18+
19+
| Action | Method & path | Auth | Request body | Success |
20+
|---|---|---|---|---|
21+
| Change password | `PUT /idp/credentials` | Owner | `{ currentPassword, newPassword }` | `200 { ok, webid, passwordChangedAt }` |
22+
| Backup / export pod | `GET /idp/account/export` | Owner || `200` `tar.gz` stream |
23+
| Delete account (API) | `DELETE /idp/account` | Owner | `{ currentPassword, purgeData? }` | `200 { ok, webid, purged }` |
24+
| Delete account (browser) | `GET` / `POST /idp/account/delete` | Owner (via form) | password field | HTML confirmation |
25+
| Passkey – register | `POST /idp/passkey/register/options`, `POST /idp/passkey/register/verify` | Session | WebAuthn ceremony ||
26+
| Passkey – login | `POST /idp/passkey/login/options`, `POST /idp/passkey/login/verify` || WebAuthn ceremony ||
27+
| Delete account (operator) | `jss account delete <username>` (CLI) | Filesystem || console output |
28+
29+
All three trio endpoints send `Cache-Control: no-store`. Re-authentication via
30+
`currentPassword` is required for both password change and deletion — possession of a token
31+
alone is not enough to perform a destructive change.
32+
33+
---
34+
35+
## Change your password
36+
37+
The authenticated owner rotates their own password. The current password must be supplied
38+
as a re-auth proof; this is verified without side effects (it does **not** stamp a login).
39+
40+
```bash
41+
curl -X PUT https://pod.example/idp/credentials \
42+
-H "Authorization: Bearer $TOKEN" \
43+
-H "Content-Type: application/json" \
44+
-d '{"currentPassword":"old-secret","newPassword":"new-stronger-secret"}'
45+
```
46+
47+
```json
48+
{
49+
"ok": true,
50+
"webid": "https://pod.example/alice/profile/card#me",
51+
"passwordChangedAt": "2026-05-27T10:30:00.000Z"
52+
}
53+
```
54+
55+
**Failure modes**
56+
57+
| Status | Meaning |
58+
|---|---|
59+
| `400` | `currentPassword` / `newPassword` missing or not strings |
60+
| `401` | Not authenticated, or `currentPassword` is wrong |
61+
| `403` | Authenticated WebID has no matching account on this server |
62+
63+
:::note Existing tokens
64+
Rotating the password does not invalidate already-issued access tokens — they reference the
65+
WebID and remain valid until they expire. The new password applies to future logins.
66+
:::
67+
68+
---
69+
70+
## Back up / export your pod
71+
72+
`GET /idp/account/export` streams a gzipped tar of the owner's entire pod tree plus a
73+
manifest. The stream is built with constant memory (`tar.pack → gzip → response`), so a
74+
multi-gigabyte pod won't exhaust server memory.
75+
76+
```bash
77+
# -OJ saves using the server-provided filename
78+
curl -L -OJ https://pod.example/idp/account/export \
79+
-H "Authorization: Bearer $TOKEN"
80+
# → jss-export-alice-2026-05-27T10-30-00-000Z-a1b2c3.tar.gz
81+
```
82+
83+
- **Content-Type:** `application/x-tar+gzip`
84+
- **Content-Disposition:** `attachment` with a timestamped, randomised filename
85+
- The archive root is `jss-export/`, containing `manifest.json` and the pod resources.
86+
87+
The `manifest.json` records who/what produced the archive:
88+
89+
```json
90+
{
91+
"webId": "https://pod.example/alice/profile/card#me",
92+
"username": "alice",
93+
"email": "alice@example.com",
94+
"podName": "alice",
95+
"mode": "multi-user",
96+
"createdAt": "2026-01-01T00:00:00.000Z",
97+
"exportedAt": "2026-05-27T10:30:00.000Z",
98+
"jssVersion": "0.0.203"
99+
}
100+
```
101+
102+
:::tip Credible Exit — your keys leave with you
103+
When the pod was provisioned with keys (`--provision-keys`), the export **intentionally
104+
includes** `/private/privkey.jsonld`. The user's secret is theirs; withholding it would make
105+
self-sovereign identity migration impossible. The endpoint is owner-authenticated, so the
106+
secret never crosses the WAC perimeter to anyone but the owner.
107+
:::
108+
109+
**Failure modes**
110+
111+
| Status | Meaning |
112+
|---|---|
113+
| `401` | Not authenticated |
114+
| `403` | No account for the caller's WebID (multi-user), or the authenticated WebID is not the seeded owner (single-user, e.g. an external Solid-OIDC / LWS identity) |
115+
| `404` | Pod directory unexpectedly missing |
116+
| `500` | Server-internal name validation regressed (defensive) |
117+
118+
**Out of scope:** re-import, cross-server pod migration, scheduled/periodic backups, and
119+
partial / per-resource selection. The export is a one-shot, whole-pod snapshot.
120+
121+
---
122+
123+
## Delete your account
124+
125+
### Via the API
126+
127+
```bash
128+
curl -X DELETE https://pod.example/idp/account \
129+
-H "Authorization: Bearer $TOKEN" \
130+
-H "Content-Type: application/json" \
131+
-d '{"currentPassword":"my-secret","purgeData":true}'
132+
```
133+
134+
```json
135+
{ "ok": true, "webid": "https://pod.example/alice/profile/card#me", "purged": true }
136+
```
137+
138+
- `purgeData: true` (optional) also removes the pod's filesystem tree at
139+
`<dataRoot>/<podName>/`. Omit it to delete only the account record and keep the data.
140+
- OIDC session cookies are expired on the response so the browser won't replay stale
141+
references on the next login.
142+
143+
**Failure modes**
144+
145+
| Status | Meaning |
146+
|---|---|
147+
| `400` | `currentPassword` missing |
148+
| `401` | Not authenticated, or `currentPassword` is wrong |
149+
| `403` | Single-user mode (deletion via HTTP is disabled — use the CLI), or no account for the caller's WebID |
150+
151+
### Via the browser
152+
153+
A no-JavaScript HTML flow is available for users without API tooling:
154+
155+
- `GET /idp/account/delete` — renders the confirmation form
156+
- `POST /idp/account/delete` — submits it (authentication happens by entering the password)
157+
158+
These responses carry anti-clickjacking headers (`X-Frame-Options: DENY`,
159+
`Content-Security-Policy: frame-ancestors 'none'`) so the destructive form cannot be embedded
160+
in a hostile iframe.
161+
162+
### Via the operator CLI
163+
164+
In **single-user mode** HTTP deletion is refused — removing the only account would brick the
165+
server until re-seed. An operator with filesystem access uses the CLI instead:
166+
167+
```bash
168+
jss account delete alice # delete account, keep pod data
169+
jss account delete alice --purge # also remove pod data
170+
jss account delete alice -y # skip the confirmation prompt
171+
jss account delete alice -r ./data # point at a specific data directory
172+
```
173+
174+
---
175+
176+
## Passkeys (WebAuthn)
177+
178+
JSS supports passkey enrolment and login alongside passwords. These endpoints implement the
179+
standard WebAuthn challenge/response ceremony:
180+
181+
- **Register:** `POST /idp/passkey/register/options` returns a registration challenge;
182+
`POST /idp/passkey/register/verify` validates the authenticator's attestation.
183+
- **Login:** `POST /idp/passkey/login/options` returns an authentication challenge;
184+
`POST /idp/passkey/login/verify` validates the assertion.
185+
186+
The bodies are produced and consumed by a browser WebAuthn client rather than hand-crafted,
187+
so they are not documented as flat JSON here. See [Authentication](./authentication.md) for
188+
the surrounding login flow.

docs/features/authentication.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ description: Solid-OIDC, Nostr NIP-98, and token authentication
88

99
JSS supports multiple authentication methods.
1010

11+
> Already have an account? See [Account Management](./account-management.md) for changing
12+
> your password, exporting/backing up your pod, and deleting your account.
13+
1114
## Simple Tokens (Development)
1215

1316
Token returned from pod creation:

docs/features/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ A drop-in alternative to `npx serve` with REST write support, live reload, and S
4747
| [Nostr NIP-98](/docs/features/nostr) | Schnorr signature-based HTTP auth, `did:nostr` identity | [Details](/docs/features/nostr) |
4848
| [End-to-End Encryption](/docs/features/e2ee) | Client-side NIP-44 / NIP-04 over `did:nostr` keys; pod stores ciphertext only | [Details](/docs/features/e2ee) |
4949
| [Invite-only Registration](/docs/features/quotas-and-invites) | Restrict signups with invite codes (`--invite-only`) | [Details](/docs/features/quotas-and-invites) |
50+
| [Account Management](/docs/features/account-management) | Self-service change password, pod backup/export, account deletion | [Details](/docs/features/account-management) |
5051
| Token Management | DPoP validation, jti replay prevention | |
5152

5253
## Developer Tools

sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const sidebars: SidebarsConfig = {
3333
'features/patching',
3434
'features/access-control',
3535
'features/authentication',
36+
'features/account-management',
3637
'features/lws',
3738
'features/websocket-notifications',
3839
'features/multi-user-pods',

0 commit comments

Comments
 (0)