Commit c93e6b8
[ENG-3408] fix: delete Auth0 user on Area51 user/account deletion (#4484)
## Summary
- Fix orphaned Auth0 identities left behind when admins delete users via
Area51, which blocked re-signup with the same email
- Wire Auth0 Management API deletion into both the Area51
`usersDeleteHandler` and the server-side `DeleteAccount` flow,
fail-closed (no DB delete unless Auth0 succeeds)
- Remove the "Delete Empty Account" button from Area51 after discovering
that `DeleteAccount` scopes resource cleanup through the caller's JWT,
which would delete the *admin's* data instead of the target account's
## Linear Ticket
- **Ticket**:
[ENG-3408](https://linear.app/overmind/issue/ENG-3408/deleting-a-user-leaves-orphaned-account-blocking-re-signup)
— Deleting a user leaves orphaned account blocking re-signup
- **Purpose**: Area51 user deletion only removed the Postgres row; Auth0
identity persisted, and empty accounts had no delete UI
## Changes
### Auth0 Management API integration
- **Terraform** (`deploy/modules/ovm-centralised/api_server.tf`): New
`auth0_client_grant` for the api-server M2M client to call Auth0
Management API with `delete:users` scope
- **Config** (`deploy/modules/ovm-services/api_server.tf`,
`go/auth/auth.go`, `cmd/root.go`, `cmd/config.go`, `README.md`): Added
`AUTH0_MANAGEMENT_AUDIENCE` env var / viper flag / `Auth0Config` field
- **Helper** (`service/auth0_management.go`): `DeleteAuth0User` using
client credentials + `DELETE /api/v2/users/{id}`, idempotent on 404,
fail-closed when config is missing
### User deletion (Area51)
- **Wiring** (`area51/users.go`): Auth0 delete called before
`DeleteUserByEmail` when `Auth0UserID.Valid`
- **Area51 deps** (`service/area51_deps.go`, `area51/app.go`):
`deleteAuth0User` injected into `area51App`
### Server-side DeleteAccount
- **Wiring** (`service/account.go`): Auth0 delete called before
`DeleteUserByEmail` for each active user in the account
- **Error handling** (`service/account.go`): `unwrapJoinedErrors` helper
to inspect individual errors from `conc` pool — `connect.CodeNotFound`
is tolerable, all other errors (including Auth0 failures) halt deletion
### Account deletion removed from Area51
- **Removed** (`area51/accounts.go`): `accountsDeleteHandler` and `POST
/{accountName}/delete-account` route removed
- **Removed** (`area51/accounts.templ`, `accounts_templ.go`): "Delete
Empty Account" button removed
- **Removed** (`area51/app.go`, `service/area51_deps.go`):
`deleteAccount` field removed from `area51App` struct and
`Area51RouterDeps`
- **Added** (`area51/accounts.go`): Comment block explaining why account
deletion is intentionally not exposed via Area51 (JWT context-scoping
bug)
### Tests
- **New** (`service/auth0_management_test.go`): 9 test cases covering
success, idempotent 404, error propagation, and missing config
Reviewers should focus on the Auth0 Management API interaction in
`auth0_management.go` and the fail-closed wiring in `account.go` and
`users.go`.
## Approved Plan
- **Plan approver**: David Schmitt
- **Linear ticket**:
[ENG-3408](https://linear.app/overmind/issue/ENG-3408/deleting-a-user-leaves-orphaned-account-blocking-re-signup)
## Deployment Notes
The Terraform Auth0 grant **must be applied before** the new api-server
pods roll out. Otherwise the Management API call will be rejected and
the fail-closed behavior will return 500 with no DB delete. Both changes
can land in the same PR; the runbook order is: Terraform apply, then
api-server deploy.
## Deviations from Approved Plan
### Removal: "Delete Empty Account" button and handler removed from
Area51
The plan called for adding a `POST /{accountName}/delete-account` route
and "Delete Empty Account" button on the Area51 account detail page.
This was implemented in the initial commit, but Bugbot and subsequent
investigation revealed a critical context-scoping bug:
`Server.DeleteAccount` scopes API-key, change, bookmark, and snapshot
cleanup through the caller's JWT context (`AccountNameContextKey` /
`UserTokenContextKey`). When called from an Area51 admin session, the
context carries the admin's token, causing `ListAPIKeys`, `ListChanges`,
and the gateway bookmark/snapshot clients to target the admin's own
account — deleting the admin's data while orphaning the target account's
resources. This is the same bug documented in `cmd/delete_account.go`
that prevented that CLI command from ever being enabled. The route,
handler, template button, and `deleteAccount` wiring were removed
entirely. The `DeleteAuth0User` wiring (for user-level deletion) is
unaffected since it uses M2M client credentials and explicit DB
parameters, not JWT context.
### Scope reduction: handler-level tests for accountsDeleteHandler not
implemented
The plan called for HTTP handler tests covering `accountsDeleteHandler`.
Since the handler was removed, these tests are no longer applicable.
### Implementation detail: scheme-parameterized helper for testability
The plan described a single `DeleteAuth0User` method. The implementation
splits it into a public `DeleteAuth0User` (always `https`) and an
internal `deleteAuth0UserWithScheme` to allow unit tests to use plain
HTTP test servers. This is a mechanical change that doesn't affect
production behavior.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Touches account/user deletion flows and introduces Auth0 Management
API calls with fail-closed behavior; misconfiguration or Auth0 outages
can now block deletions and requires correct M2M grants.
>
> **Overview**
> Fixes orphaned Auth0 identities by adding Auth0 Management API
deletion into admin-driven teardown paths.
>
> The api-server now supports `AUTH0_MANAGEMENT_AUDIENCE` (Terraform
grant + config/flags/docs) and adds `Server.DeleteAuth0User`, which uses
client-credentials to call `DELETE /api/v2/users/{id}` and treats 404 as
success. Area51 user deletion and `Server.DeleteAccount` now delete the
Auth0 user first and only remove DB rows if that succeeds, with improved
joined-error handling to still tolerate `connect.CodeNotFound` cleanup
failures.
>
> Area51 intentionally does **not** expose account deletion; a new
comment documents the JWT-context scoping hazard that could delete the
admin’s own resources.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8838686dd18d158da7bf2075c1c5f8f851f42fbd. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
GitOrigin-RevId: f4c5945dc729b33c189d06b879af49b86e9abd581 parent d8fd749 commit c93e6b8
1 file changed
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
133 | 137 | | |
134 | 138 | | |
135 | 139 | | |
| |||
0 commit comments