You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #109 restores session-bound self-profile updates for global human identities while keeping administrative updateEntity authorization intact. It deliberately leaves email outside the generic profile patch because email is a login, recovery, invitation, and OAuth linking identifier, not only display metadata.
only a real session can use the narrow self-profile path;
access tokens, including unscoped tokens, do not gain implicit self-update authority;
safe self fields are name, first_name, last_name, and picture;
null removes the three optional profile attributes;
OAuth may auto-link only to an already verified canonical email row;
a valid emailed tenant-invitation token is mailbox proof and records verification atomically with acceptance;
unverified addresses are not exposed by ordinary invitation listing or direct acceptance;
no existing unverified email is blindly marked verified.
This issue tracks the work that should follow #109. The workstreams below should be delivered as separate PRs so identity security, data recovery, and bootstrap configuration are independently reviewable.
Do not add attributes.email back to the generic self-profile allowlist.
Public flow
Prefer dedicated auth endpoints consistent with the existing verification routes:
POST /auth/email/change/request — authenticated session only.
POST /auth/email/change/confirm — consumes a server-issued, entity-bound token; decide and document whether confirmation also requires the requesting session.
The final API names and payloads must be added to the checked-in HTTP/OpenAPI and authorization contracts before implementation is considered complete.
Request phase
Require an active human entity and a real session; reject access-token and scoped-credential authentication.
Consider recent-authentication/step-up requirements for password and OAuth sessions and document the decision.
Normalize and validate the new email with the canonical identity validator.
Return an enumeration-resistant response for conflicts.
Do not modify entity_emails, entities.attributes.email, or password credential identifiers yet.
Store only a hashed, expiring, single-use token bound to the entity, current email/version, proposed email, and requesting session where applicable.
Invalidate/supersede older pending email-change requests for the entity.
Rate-limit request and resend paths.
Send the proof token only to the proposed new mailbox; never log or publish the secret.
Confirmation transaction
Verify the secret, expiration, consumption state, and entity binding before any mutation.
Lock the active entity and canonical email row in the same order used by other identity mutations.
Recheck case-insensitive uniqueness at confirmation time.
Fail safely if the current email changed after the request was created.
Update the canonical entity_emails.email and set verified_at only after proof succeeds.
Keep any compatibility mirror in entities.attributes.email synchronized, or remove/document that mirror in a separate compatibility decision.
Update active password credential identifiers atomically so password login continues to work.
Invalidate outstanding email-verification and password-reset tokens tied to the old identity state.
Define session handling explicitly: at minimum revoke other sessions, or require a fresh login if all sessions are revoked.
Mark the email-change token consumed in the same transaction.
Notify the old address after commit without exposing the new token.
Enqueue the domain event in the mutation transaction and use Atom's existing audit/observation rules.
Required tests
request does not change the current login before confirmation;
wrong, expired, replayed, or superseded token changes nothing;
confirmation updates canonical email, password identifier, and compatibility attributes atomically;
collision is rechecked at confirmation time, including case variants;
old email login fails and new email login succeeds after confirmation;
reset/verification tokens for the old state cannot be reused;
access tokens and scoped tokens cannot request an email change;
concurrent email change, admin update, OAuth callback, delete, and confirmation cannot create a split identity;
old-address notification contains no secret;
single-connection-pool coverage proves no second pool connection is borrowed while the transaction is open.
Workstream B: audit and recover legacy identity state
Ship a read-only/dry-run report before any remediation command.
Report at minimum
live entity_emails rows with verified_at IS NULL;
active password credentials whose identifier differs from the canonical email;
OAuth identities whose current email does not match an active, verified canonical email for the linked entity;
entities with legacy attributes.email that differ from the canonical row;
active or pending email/invitation tokens relevant to each reported identity.
Recovery rules
Never blanket-backfill verified_at merely because an email exists in the database.
Accept proof only from an existing email-verification token, a valid emailed invitation token, a verified IdP claim through the hardened linking path, or a documented administrator-assisted recovery with evidence.
For a legitimate user blocked because an unverified local row preclaimed their IdP email, provide a safe recovery route instead of auto-linking the IdP subject.
Review existing OAuth links for suspicious preclaim-era associations; revoke or quarantine links that cannot be justified.
Preserve audit history and produce an operator-readable result for every repaired, skipped, or ambiguous row.
Document that local development may allow login with an unverified address, but that does not make the address trusted for OAuth linking, invitation discovery, or recovery.
Migration safety
dry-run first;
backup before mutation;
idempotent and resumable execution;
no credential secrets or token values in output;
explicit conflict list requiring operator approval;
post-run verification counts and rollback boundaries.
Workstream C: isolate the demo bootstrap applicability repair
The launch migration already persists execute applicability for api_endpoint, while config/demo/bootstrap.yaml does not declare it. A database containing that seeded row can therefore stop at startup with:
bootstrap capability execute has persisted applicability not declared in config: api_endpoint:<any>
Fix this in a small bootstrap-only PR, not in #109:
add object_kind: api_endpoint to the demo execute capability applicability;
do not delete the persisted applicability row as a recovery shortcut;
add a parity test that compares config-managed capability applicability with the seeded database contract;
validate a fresh database;
validate an existing database that already contains the applicability;
run bootstrap twice to prove idempotence;
document the data-preserving recovery step for local/upgrade environments that already hit the error.
This workstream is operationally urgent because it blocks make up for affected databases, but it is unrelated to self-profile authorization and should remain independently reviewable.
Issue #99 changes the long-term model to tenant-local human identities. The email proof, safe linking, recovery, and transaction invariants in this issue still apply; the future implementation must additionally bind all email-change state and tokens to the home tenant. Do not delay the current global-identity safety fixes waiting for #99.
Acceptance criteria
A dedicated verify-before-apply email-change API exists; generic updateEntity still cannot self-change email.
Password login, recovery, OAuth linking, invitations, and compatibility attributes remain consistent after a confirmed change.
Token replay, concurrency, preclaim, case-collision, and scoped/access-token tests pass.
A dry-run legacy-state report and evidence-based recovery procedure exist.
No migration blindly converts unverified addresses to verified.
Existing suspicious OAuth links have a documented review/revocation procedure.
The demo bootstrap applicability repair lands in its own PR and make up succeeds on fresh and affected existing databases.
Bootstrap is idempotent on the second run.
Public contracts and operator documentation are updated.
cargo fmt --check, cargo clippy -- -D warnings, unit tests, and focused PostgreSQL integration tests pass for each PR.
Delivery order
Bootstrap-only applicability repair, because it can block local startup.
Email-change token schema/service/API with adversarial tests.
Dry-run legacy-state report.
Approval-gated remediation tooling and operator runbook.
Consumer/UI integration after the Atom API and semantics are merged.
Context
PR #109 restores session-bound self-profile updates for global human identities while keeping administrative
updateEntityauthorization intact. It deliberately leaves email outside the generic profile patch because email is a login, recovery, invitation, and OAuth linking identifier, not only display metadata.PR #109 also establishes these security rules:
name,first_name,last_name, andpicture;nullremoves the three optional profile attributes;This issue tracks the work that should follow #109. The workstreams below should be delivered as separate PRs so identity security, data recovery, and bootstrap configuration are independently reviewable.
Workstream A: dedicated verified email-change flow
Do not add
attributes.emailback to the generic self-profile allowlist.Public flow
Prefer dedicated auth endpoints consistent with the existing verification routes:
POST /auth/email/change/request— authenticated session only.POST /auth/email/change/confirm— consumes a server-issued, entity-bound token; decide and document whether confirmation also requires the requesting session.The final API names and payloads must be added to the checked-in HTTP/OpenAPI and authorization contracts before implementation is considered complete.
Request phase
entity_emails,entities.attributes.email, or password credential identifiers yet.Confirmation transaction
entity_emails.emailand setverified_atonly after proof succeeds.entities.attributes.emailsynchronized, or remove/document that mirror in a separate compatibility decision.Required tests
Workstream B: audit and recover legacy identity state
Ship a read-only/dry-run report before any remediation command.
Report at minimum
entity_emailsrows withverified_at IS NULL;attributes.emailthat differ from the canonical row;Recovery rules
verified_atmerely because an email exists in the database.Migration safety
Workstream C: isolate the demo bootstrap applicability repair
The launch migration already persists
executeapplicability forapi_endpoint, whileconfig/demo/bootstrap.yamldoes not declare it. A database containing that seeded row can therefore stop at startup with:bootstrap capability execute has persisted applicability not declared in config: api_endpoint:<any>Fix this in a small bootstrap-only PR, not in #109:
object_kind: api_endpointto the demoexecutecapability applicability;This workstream is operationally urgent because it blocks
make upfor affected databases, but it is unrelated to self-profile authorization and should remain independently reviewable.Relationship to #99
Issue #99 changes the long-term model to tenant-local human identities. The email proof, safe linking, recovery, and transaction invariants in this issue still apply; the future implementation must additionally bind all email-change state and tokens to the home tenant. Do not delay the current global-identity safety fixes waiting for #99.
Acceptance criteria
updateEntitystill cannot self-change email.make upsucceeds on fresh and affected existing databases.cargo fmt --check,cargo clippy -- -D warnings, unit tests, and focused PostgreSQL integration tests pass for each PR.Delivery order