Skip to content

idp: self-service DELETE /idp/account endpoint (#352)#391

Merged
melvincarvalho merged 5 commits into
gh-pagesfrom
issue-352-self-delete-account
May 9, 2026
Merged

idp: self-service DELETE /idp/account endpoint (#352)#391
melvincarvalho merged 5 commits into
gh-pagesfrom
issue-352-self-delete-account

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Closes #352.

Summary

  • New endpoint DELETE /idp/account lets an authenticated owner delete their own account.
  • Same auth shape as PUT /idp/credentials (Add HTTP endpoint for end users to change their own password #351): caller's existing token + body re-confirms currentPassword. Account is resolved from the caller's WebID, so cross-account attacks (A authenticated, sends B's password) return 401 and don't touch anything.
  • Optional purgeData: true removes the pod's filesystem tree at <dataRoot>/<username>/, mirroring the CLI's --purge semantics. Default is false (matches CLI default).
  • Single-user mode rejects with 403 — deleting the single account would brick the IdP until re-seed; CLI (jss account delete) stays the operator path.

Endpoint

DELETE /idp/account
  Headers: Authorization (existing session / DPoP)
  Body:    { currentPassword: string, purgeData?: boolean }
Returns:
  200 { ok, webid, purged }   — success
  400                         — missing/invalid currentPassword
  401                         — unauthenticated, or wrong password
  403                         — single-user mode, or no account for caller's WebID

Rate-limited 5/min/IP.

Out of scope (deferred to follow-ups)

  • HTML UI at /idp/account/delete — the issue allows it; auth for an /idp HTML page is its own concern (existing /idp landing is unauthenticated). Endpoint + tests is the unit of value here. Doctor-app or pane work in a follow-up.
  • In-flight token invalidation — bearer tokens that don't round-trip through findByWebId() will appear valid until they expire. Same shape as the password-rotation endpoint; tracked elsewhere if needed.
  • Operator-via-HTTP deletion of other users' accounts — explicitly out of scope per the issue. Stays CLI-only.

Files

  • src/idp/credentials.jshandleDeleteAccount(request, reply, { singleUser }), ~110 LOC
  • src/idp/index.js — route registration, ~14 LOC
  • test/idp-delete-account.test.js — 8 new tests, ~300 LOC

Test plan

  • npm test passes (currently 627/627 locally)
  • Manual: create a pod, login, curl -X DELETE … -d '{"currentPassword":"..."}' → 200, subsequent login → 401
  • Manual: same with "purgeData": true → pod tree gone

Refs

Authenticated owner can now delete their own account via HTTP. Mirrors
the password-rotation pattern from #351 (PUT /idp/credentials):
caller authenticates with their existing session, re-proves possession
by sending currentPassword in the body, server resolves the account
from the caller's WebID and deletes it. Owner-only by construction —
the cross-account attack (A authenticated, sends B's password) returns
401 and touches nothing.

Two new shape elements vs. password rotation:

1. Single-user mode rejects with 403. Deleting the single account
   would brick the IdP until re-seed; the operator path stays the CLI
   (`jss account delete`). Test asserts both 403 status and
   account-still-functional after.

2. Optional `purgeData: true` body flag mirrors the CLI's `--purge`.
   Removes the pod's filesystem tree at <dataRoot>/<username>/. Path
   is normalized + verified to be a proper child of dataRoot
   (belt-and-suspenders against future config drift; usernames are
   already validated by registration). Default is false — account
   record gone, pod data preserved, matches the CLI's default.

Endpoint surface:
  DELETE /idp/account
    Headers: Authorization (existing session / DPoP)
    Body:    { currentPassword: string, purgeData?: boolean }
  Returns:
    200 { ok, webid, purged }   — success
    400                         — missing/invalid currentPassword
    401                         — unauthenticated, or wrong password
    403                         — single-user mode, or no account for caller's WebID
  Rate-limited 5/min/IP.

UI deferred — the issue allows it; auth for a /idp HTML page is its
own concern (existing /idp landing is unauthenticated). Endpoint +
tests is the unit of value here. Doctor-app or pane work in a follow-up.

Tests cover: unauthenticated, missing field, wrong password, happy
path (subsequent login fails), purgeData=true wipes filesystem,
purgeData=false preserves it, cross-account safety, single-user 403.
8 new tests; 627/627 full suite passes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a self-service IdP endpoint for authenticated users to delete their own account (with optional pod data purge), aligned with the existing “re-enter currentPassword” pattern used for password rotation.

Changes:

  • Register DELETE /idp/account with per-IP rate limiting.
  • Implement handleDeleteAccount() to authenticate by WebID, verify currentPassword, delete the account record, and optionally purge <DATA_ROOT>/<username>/.
  • Add a dedicated test suite covering success and key failure modes (unauthenticated, missing password, wrong password, cross-account attempt, purge behavior, single-user mode rejection).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/idp/index.js Registers the new DELETE /idp/account route with rate limiting and passes singleUser mode into the handler.
src/idp/credentials.js Adds handleDeleteAccount() implementing owner-only deletion and optional filesystem purge.
test/idp-delete-account.test.js Adds integration tests for the new endpoint, including purge/no-purge and single-user mode behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/idp/credentials.js Outdated
* 403 — single-user mode (deletion would brick the server until
* re-seed; operator should use the CLI), or no account for the
* caller's WebID
* 404 — no account at all (already deleted)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 471f759: JSDoc no longer mentions 404. The 'no account for caller's WebID' case correctly lands at 403 (the caller has a valid token, they're identifying themselves — just not for an account this server holds). Comment updated with the rationale.

Comment thread src/idp/credentials.js
Comment on lines +359 to +378
// 5. Delete the account record + indexes
await deleteAccount(account.id);

// 6. Optionally purge the pod's filesystem data. Mirrors the CLI
// `--purge` semantics. The path is <dataRoot>/<username>/, with
// username already validated at registration (#321 alphanum/dash/dot
// rules) so no traversal risk in practice; defensive normalize
// anyway.
let purgedPath = null;
if (purgeData) {
const dataRoot = process.env.DATA_ROOT || './data';
const candidate = path.resolve(dataRoot, account.username);
const root = path.resolve(dataRoot);
// Belt-and-suspenders: refuse to remove anything that isn't a
// proper child of the data root. Won't trigger on registered
// usernames; protects against config drift / future bugs.
if (candidate.startsWith(root + path.sep) && candidate !== root) {
await fs.remove(candidate);
purgedPath = candidate;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real concern. Fixed in 471f759: purge is now best-effort. The account is deleted before the purge attempt, so a throw was previously surfacing as a 500 over a partially-completed state. Now wrapped in try/catch — on throw the operator log gets the full error via request.log.error (err / path / username) and the response carries `purged: false` so the caller knows pod data may still exist. Raw err message not surfaced to the user (file paths / permission details are sensitive). Operator can finish cleanup with the CLI `--purge` against the orphaned directory.

Two fixes per the review:

1. JSDoc no longer claims a 404 failure mode the code doesn't return.
   "No account for caller's WebID" lands at 403, not 404 — the caller
   had a valid token (so they're identifying themselves), just not for
   an account this server holds. Added the rationale to the comment.

2. Purge is now best-effort. fs.remove() can throw (permissions, FS
   races, etc.); the account is deleted *before* the purge, so a
   throw would have surfaced as a 500 over a partially-completed
   deletion (account gone, pod data lingering). New shape:
   - try/catch around fs.remove
   - on throw: log server-side via request.log.error (with err / path
     / username for the operator log) and continue with purged: false
   - response.purged accurately signals outcome; an operator can
     finish the cleanup with the CLI `--purge` against the orphaned
     directory if needed
   - raw err message NOT surfaced to the user — file paths and
     permission detail are sensitive

8/8 delete-account tests still pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread src/idp/credentials.js
Comment on lines +377 to +380
const dataRoot = process.env.DATA_ROOT || './data';
const candidate = path.resolve(dataRoot, account.username);
const root = path.resolve(dataRoot);
// Belt-and-suspenders: refuse to remove anything that isn't a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real bug, fixed in 82589e7. createAccount() lowercases the username (`username.toLowerCase().trim()`) but `account.podName` is stored as-is. On case-sensitive FS the pod dir is at `/Alice/` while `account.username` is `alice` — derived purge path didn't match. Fixed by using `account.podName` (with username fallback for older records). Same bug fixed at bin/jss.js:776 since the CLI's `--purge` had the identical issue (only hidden on case-insensitive devboxes like macOS default).

Comment thread src/idp/credentials.js Outdated
Comment on lines +365 to +366
// username already validated at registration (#321 alphanum/dash/dot
// rules) so no traversal risk in practice; defensive normalize

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 82589e7. The comment was describing the slug regex (which allows dots), not the pod-name regex `/^[a-zA-Z0-9_-]+$/` at handlers/container.js:273 — alphanum + dash + underscore, no dots, allows uppercase. Comment now matches reality.

assert.strictEqual(await fs.pathExists(podPath), false,
'pod data should be purged');
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 82589e7: new test "purgeData: true removes the on-disk pod dir even when it has uppercase letters" creates a pod with mixed-case name `Greta${ts}`, verifies the on-disk dir at the mixed-case path is gone after purge. Would fail on the previous code path on a case-sensitive filesystem. 9/9 delete-account tests now pass.

Real bug. createAccount() in src/idp/accounts.js normalizes username
to lowercase (`username.toLowerCase().trim()`), but the pod directory
on disk is created using the original-case input to handleCreatePod.
On case-sensitive filesystems these can differ:

  Account record: { username: "alice", podName: "Alice" }
  On disk:        <dataRoot>/Alice/

The previous purge logic derived its path from account.username, so
it would either no-op (path didn't exist) or hit a different
directory if one happened to exist at the lowercased name. Both
outcomes are wrong: pod data lingers, or the wrong tree is removed.

Fix: use account.podName, falling back to account.username only if
podName is somehow absent (older account records or edge cases).
Same fix applied to the CLI at bin/jss.js:776 — same code path,
same bug, would only have been hidden on case-insensitive devboxes
(macOS default).

Test: new "purgeData: true removes the on-disk pod dir even when it
has uppercase letters" — creates a pod with mixed-case name "Greta…",
verifies the on-disk dir at the mixed-case path is gone after purge.
This would fail on the previous code path on a case-sensitive FS.

Also corrected the inline comment about pod-name validation: it was
describing the slug regex (allows dots), not the pod-name regex
(/^[a-zA-Z0-9_-]+$/, no dots, allows underscores and uppercase).

9/9 delete-account tests pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.

Comment thread src/idp/credentials.js Outdated
* Owner-initiated account deletion. Authenticated caller proves
* possession via re-entering currentPassword (matches the
* password-rotation pattern in #351). Optional `purgeData: true` also
* removes the pod's filesystem tree at <dataRoot>/<username>/.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2e059c9: JSDoc now says `//` to match what the code actually purges (account.podName, falling back to username only if podName is absent).

Comment thread src/idp/credentials.js Outdated
Comment on lines +387 to +390
// Belt-and-suspenders: refuse to remove anything that isn't a
// proper child of the data root. Won't trigger on registered
// pod names; protects against config drift / future bugs.
if (candidate.startsWith(root + path.sep) && candidate !== root) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real edge case. Fixed in 2e059c9 by switching from `candidate.startsWith(root + path.sep)` to `path.relative(root, candidate)`. The startsWith check would fail-closed when DATA_ROOT is a filesystem root like `/` (root is 1 char, root + path.sep is 2 chars, no valid child path matches). path.relative handles that correctly and tightens the `..` traversal check up-front in the same expression.

Two fixes:

1. JSDoc no longer says `<dataRoot>/<username>/` — corrected to
   `<podName>` to match the implementation (which now uses
   account.podName, falling back to username only if podName is
   absent).

2. The "candidate is a proper child of dataRoot" guard switched from
   `candidate.startsWith(root + path.sep)` to `path.relative(root,
   candidate)`. The startsWith check failed at filesystem roots: if
   DATA_ROOT is `/`, then `root` is `/` (1 char) and `root + path.sep`
   is `//` (2 chars), so any legitimate child like `/alice` fails the
   prefix check and the purge silently no-ops. path.relative handles
   that correctly — and rejects `..` traversal up front, which is
   tighter anyway.

9/9 delete-account tests still pass.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.

Comment thread src/idp/credentials.js Outdated
* Owner-initiated account deletion. Authenticated caller proves
* possession via re-entering currentPassword (matches the
* password-rotation pattern in #351). Optional `purgeData: true` also
* removes the pod's filesystem tree at <dataRoot>/<username>/.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 88a40cb. There were two JSDoc lines mentioning the path; I corrected the inline comment in pass 3 but missed the function-level summary at the top of handleDeleteAccount. Now both say <podName> (falling back to <username> if podName is absent).

…ame>

I caught the inline comment in pass 3 but missed this higher up in the
function-level JSDoc. Same correction: <username> → <podName> with
fallback note.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated no new comments.

@melvincarvalho
melvincarvalho merged commit aaeae1e into gh-pages May 9, 2026
4 checks passed
@melvincarvalho
melvincarvalho deleted the issue-352-self-delete-account branch May 9, 2026 05:06
melvincarvalho added a commit that referenced this pull request May 9, 2026
Three small fixes:

1. handleAccountDeleteForm JSDoc clarifies it's POST-only; GET is a
   separate route in index.js that just renders the form via
   accountDeletePage. Pass-1 wording was 'GET / POST' which implied
   both routes flow through this handler.

2. accountDeletePage's JSDoc still mentioned 'identifier (username or
   email)' from before the email simplification in pass 2. Now says
   'username + password' to match the actual UI.

3. The checkbox copy promised 'Removes <dataRoot>/<username>/' but
   the purge path is derived from account.podName (which can differ
   from the lowercased username on case-sensitive FS, per #391
   pass 2). Softened to 'Removes the pod folder on disk' — accurate
   without leaking the path shape.

17/17 tests pass.
melvincarvalho added a commit that referenced this pull request May 9, 2026
* idp: HTML form for self-service account deletion (#392)

Human-friendly UI on top of the JSON DELETE /idp/account endpoint
(#391). Public unauthenticated page at /idp/account/delete; auth
happens at submission time via username + password. Matches the
existing /idp landing and /idp/register pattern — no JavaScript
required, accessibility-first form-encoded POST.

## Form fields
- Username or email
- Current password (re-entry as proof of possession)
- Type your username again to confirm (destructive-action UX guard)
- Optional checkbox: also delete pod data

## Routes added
- GET  /idp/account/delete  → render the form (or single-user disabled message)
- POST /idp/account/delete  → process submission

Same 5/min/IP rate-limit as the JSON endpoint. Single-user mode shows
the disabled-message page instead of the form on both GET and POST,
matching the JSON endpoint's 403 policy. Operator path remains the
CLI (`jss account delete`).

## Internal refactor
Factored the shared "delete + optional purge" logic into
`deleteAccountAndOptionallyPurge(request, account, purgeData)` so the
JSON endpoint and form endpoint share the same path. Pod-data purge
uses `account.podName` (per #391 pass 2) and path.relative (per #391
pass 3) so the case-sensitivity fix and FS-root edge case carry over.

## Tests added (8)
- GET renders form HTML with all required fields
- POST happy path → success page + login fails after
- POST with purgeData=on wipes pod tree
- POST with mismatched confirmUsername → form re-rendered with error
  + username pre-filled for retry, account untouched
- POST with wrong password → form re-rendered with error, account
  untouched
- POST with missing fields → form re-rendered with error
- Single-user mode: GET renders disabled message (no form)
- Single-user mode: POST also renders disabled message — does NOT
  delete (verified: account still functional after the POST)

636/636 full suite passes.

* idp: address Copilot review on #393 — finish refactor + honest copy

Six fixes per the review:

1. handleDeleteAccount now actually uses the new
   deleteAccountAndOptionallyPurge helper. The helper was added in the
   prior commit but the original function still had its own inline
   purge block — divergence risk. Now both endpoints share the same
   path verbatim.

2. JSDoc reference \`handleAccountDeletePost\` → \`handleAccountDeleteForm\`
   (the actual exported name). Two route methods (GET + POST) live in
   the same handler; doc updated to reflect that.

3. JSDoc claimed failures "redirect back to the GET form with an
   error message" — the implementation actually re-renders the form
   in-place with status 200. Doc corrected to match (single response,
   no redirect).

4. Form handler now respects the \`purged\` return from the helper.
   When the user requested purgeData but the purge didn't run (fs
   threw, path-relative check rejected), the success page surfaces a
   "your account was deleted but the pod-data purge did not complete"
   notice. Account deletion succeeded — that part can't be undone —
   but the operator may need to finish cleanup.

5. Warning copy on the form softened: was "any active sessions will
   be removed", which the server doesn't actually implement.
   Now: "future sign-ins with this username will fail" + an italic
   note that already-issued access tokens may remain usable until
   their expiry.

6. Same correction on the success-page copy.

17/17 delete-account tests still pass.

* idp: Copilot review pass 2 on #393 — JSDoc accuracy, drop email-or-username UX confusion

Four polish fixes:

1. handleAccountDeleteForm reference in deleteAccountAndOptionallyPurge's
   JSDoc was still mis-named handleAccountDeletePost (one stray spot the
   pass-1 fix missed).

2. accountDeletePage's JSDoc updated: "redirect-back-with-error" was
   wrong (we re-render in place); also documented the new purgeFailed
   param added in pass 1.

3. + 4. Form label simplified from "Username or email" → "Username".
   Confirms a user point: JSS doesn't really do email — no SMTP, no
   verification flow, no recovery flow, the HTML register form doesn't
   ask for email at all. The "email" field in accounts.js is a
   data-layer artifact (auto-generates <username>@jss if absent) and
   isn't surfaced anywhere user-facing. Saying "username or email" on
   this form mismatched reality and confused the destructive-action
   confirm field ("Type your username again to confirm"). Now both
   fields consistently say username, matching what users will actually
   enter.

Server-side authenticate() still tries findByUsername then
findByEmail for back-compat with anyone who registered programmatically
with a real email; that fallback is just unreachable from this form
since the field never accepts email anymore. Cleaning that up across
the wider auth surface is a separate PR.

17/17 delete-account tests still pass.

* idp: pass 3 on #393 — JSDoc / copy accuracy

Three small fixes:

1. handleAccountDeleteForm JSDoc clarifies it's POST-only; GET is a
   separate route in index.js that just renders the form via
   accountDeletePage. Pass-1 wording was 'GET / POST' which implied
   both routes flow through this handler.

2. accountDeletePage's JSDoc still mentioned 'identifier (username or
   email)' from before the email simplification in pass 2. Now says
   'username + password' to match the actual UI.

3. The checkbox copy promised 'Removes <dataRoot>/<username>/' but
   the purge path is derived from account.podName (which can differ
   from the lowercased username on case-sensitive FS, per #391
   pass 2). Softened to 'Removes the pod folder on disk' — accurate
   without leaking the path shape.

17/17 tests pass.

* idp: pass 4 on #393 — fix body-parsing comment to match reality

The comment claimed @fastify/formbody puts fields directly on
request.body, but no such plugin is installed. JSS's wildcard
parseAs:'buffer' content parser at server.js:190 means request.body
arrives as a Buffer; the URLSearchParams parse below is what's
actually load-bearing. Comment updated to describe what the code
genuinely does.

* idp: pass 5 on #393 — side-effect-free verify, honest copy, 403 single-user, flip purge default to ON

Five fixes (4 from Copilot, 1 from maintainer call to flip the default):

1. handleAccountDeleteForm now uses findByUsername + verifyPassword
   instead of authenticate(). authenticate() writes lastLogin as a
   side effect, which is wrong shape for a destructive proof-of-
   possession check (and would fail the deletion if the account file
   weren't writable). Mirrors handleChangePassword and the JSON
   handleDeleteAccount.

2. Warning copy corrected. Previous text said "any active sessions
   will be removed" + "WebID URI will become a tombstone" — the
   server does neither. Now: "Future sign-ins with this username will
   fail" + "external links to your WebID URI will become dangling"
   (since pod data is being wiped, the URI no longer resolves).

3. Single-user mode now returns 403 from both GET and POST routes
   (matched HTML body kept). Consistency: /idp/register's
   disabled-route policy is 403, the JSON DELETE endpoint is 403,
   and now the form path is too.

4. Body-parsing comment corrected: was claiming @fastify/formbody is
   registered (it isn't); JSS uses parseAs:'buffer' for `*` so body
   arrives as a Buffer and the URLSearchParams parse is the
   load-bearing logic.

5. Form's purge default flipped from off → ON. The checkbox is now
   `keepData` (inverse of the JSON endpoint's `purgeData`), default
   unchecked = purge-on. A user filling out the form is leaving the
   server; wiping their pod data with the account is the expected
   shape, and "Keep my pod data on this server" is the explicit
   opt-out. The JSON DELETE endpoint keeps `purgeData: false` as the
   default (matches the CLI's --purge being opt-in for operator
   scripts; programmatic callers shouldn't accidentally wipe data).
   Form and JSON endpoint diverge deliberately because the use cases
   differ.

Tests:
  - happy path now expects pod data wiped by default + login fails
  - new test for keepData=on (opts out of purge; account still
    deleted, pod data preserved)
  - single-user GET / POST now expect 403
  - form-renders test now checks for name="keepData"

17/17 delete-account tests pass.

* idp: pass 7 on #393 — conditional warning copy, log path-safety rejection, fix confirm comment

Three fixes:

1. Warning copy now describes the conditional behavior accurately:
   'By default your pod data is also wiped — check the box below if
   you want to keep it.' Previous wording said pod data 'will be
   removed' unconditionally, which becomes wrong if the user checks
   the keep-data opt-out.

2. Path-safety rejection (when isProperChild is false) now logs a
   warn with path/dataRoot/podName context so operators can diagnose
   any 'purge did not complete' message that wasn't a fs.remove
   throw. No path leaks to the client; signal stays in server logs.

3. Comment on the destructive-action UX guard was misleading. It
   claimed the case-sensitive compare prevents typos hitting a
   different (lowercased) record — but findByUsername lowercases
   internally, so 'Alice' + 'Alice' both resolve to the 'alice'
   record. The compare is purely the typing-it-twice confirmation
   pattern (typo catch / accidental-submit guard); doesn't gate which
   record gets deleted. Comment now reflects what the code actually
   does.

17/17 tests pass.

* idp: pass 8 on #393 — anti-clickjacking + no-store headers on every delete-form response

Real security findings. The delete-account form takes a current-password
re-entry and the success/error pages echo state — neither should be
cacheable or embeddable in an iframe. Both threats (cache replay,
clickjacking) are real on a destructive endpoint.

Added a small exported helper:

  setNoCacheClickjackHeaders(reply):
    Cache-Control: no-store
    Pragma: no-cache
    X-Frame-Options: DENY
    Content-Security-Policy: frame-ancestors 'none'

Wired into:
  - GET /idp/account/delete (form + 403 disabled-message variant)
  - POST handler entry point — covers every return path (success,
    error re-render, single-user 403, missing-field, mismatched
    confirm, wrong password)

Tests added (2):
  - GET response carries all four headers
  - POST response carries them on both error (missing fields) and
    success (full delete) paths

19/19 delete-account tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add HTTP endpoint + UI for end users to delete their own account

2 participants