DEV Community

ThomasMoore157
ThomasMoore157

Posted on

GDPR Account Deletion and Identity Linking — Resolve, Inspect, Attach Safely

The page that wakes the on-call engineer says “account deletion completed,” but a session from a delivery driver's old phone is still accepted. That is the incident shape for an identity-linking workflow: the account row is gone, one external identity was attached to the wrong user, or a revocation job raced a login callback before anyone could resolve the identity first.

Short answer: model account deletion and identity linking as separate, auditable state transitions; resolve an external identity first, inspect the candidate and existing identities second, and attach only after an explicit, idempotent decision. For a team migrating away from a managed provider, keep that workflow provider-neutral and make the provider choice subordinate to those invariants.

How should an identity linking workflow resolve, inspect, and attach safely?

Start with a durable command, not a button handler. A request to link a Google subject, an email identity, or a carrier SSO subject enters requested. The resolver reads the external assertion and produces a stable identity key. It does not decide that two accounts are “probably the same.” That distinction is the boundary between evidence and policy.

The next state is resolved. Store the issuer, subject, and the resolver's request ID in an audit record. Then inspect the account that would receive the identity and the identities already attached to it. The available interfaces map cleanly to that sequence: POST /v1/auth/identity/resolve establishes the external identity, while GET /v1/auth/identity/list/{user_id} shows the complete set for a user. Use the documented method and path exactly; an invented REST-shaped route is a migration bug waiting to happen.

Only an operator or a policy service should move resolved to attached. The transition checks that the identity key is not already attached to another user, records who made the decision, and writes an immutable event. A user can have several identities. The same identity cannot belong to two users, and a retry must not create a second attachment.

That last sentence deserves its own line.

The deletion flow uses the same discipline. Mark the account deletion_pending, enumerate its sessions and identities, revoke sessions, remove identities, and only then delete the account record. Before unlinking the last identity, check that a password, recovery factor, or another permitted login method remains. If none remains, stop and ask for an explicit recovery or deletion decision; silently locking the user out is not a GDPR success condition.

Measure first.

In a real migration, that sequence runs beside a callback consumer, a session cache, and a legal-retention ledger. Suppose a driver confirms deletion while a new OAuth callback is queued: the callback must observe deletion_pending and be rejected, the revocation worker must record its completion, and the final delete must wait until both events are durable. If the worker is retried after a network timeout, its operation ID should select the existing audit event instead of creating another one. A nightly reconciliation can compare the identity ledger with the provider export and flag drift for review. None of this requires guessing at a vendor's internal state; it requires a state machine whose transitions have owners, timestamps, and evidence that another service can query.

Work backward from the alert, not forward from the endpoint

An SRE review should begin with what the alert can prove. For this logistics workflow, a useful alert is “deletion requested for 15 minutes without a terminal audit event,” paired with a metric for sessions still verifying after the revoke transition. The first signal catches a stuck state; the second catches a false sense of completion.

Work backward from those signals to instrumentation. Emit a counter for each transition (requested, resolved, attached, revoked, deleted), a histogram for time between transitions, and a gauge for pending deletions. Include a correlation ID, user ID, identity issuer, and request ID, but never log the raw bearer token or an unredacted external assertion. OWASP's authentication guidance is a useful baseline for handling those secrets and for treating authentication events as security-sensitive records.

Thresholds need capacity context. If the resolver normally completes in 400 ms and the queue can absorb 20 requests per second, a five-minute alert window may be noisy during a carrier outage; a fifteen-minute window may be too slow when a legal deletion deadline is near. I am not sure which value fits your traffic without the queue's percentile data, so measure p95 and p99 first, then set the SLO and page threshold from that evidence.

False positives cost attention. A page every time a user abandons the confirmation screen will train the team to ignore the one deletion that really left a live session behind.

Here is the smallest probe I use during a migration rehearsal. It deliberately treats the request schema as an input contract owned by the adapter, checks every response, and backs off on rate limiting. The endpoint is useful only after you have filled payload with the fields in the live schema; the important part here is the observable request boundary.

package main

import (
    "bytes"
    "fmt"
    "io"
        "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    payload := []byte(`{}`)
    baseURL := os.Getenv("INFRAI_API_BASE_URL")
    if baseURL == "" {
        panic("INFRAI_API_BASE_URL is required")
    }
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest("POST", baseURL+"/auth/identity/resolve", bytes.NewReader(payload))
        if err != nil { panic(err) }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", "migration-probe-identity-001")
        resp, err := http.DefaultClient.Do(req)
        if err != nil { panic(err) }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { panic(readErr) }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Second
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay * time.Duration(1<<attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("resolve failed (%d): %s", resp.StatusCode, body))
        }
        fmt.Println(string(body))
        return
    }
    panic("resolve rate limit did not clear after retries")
}
Enter fullscreen mode Exit fullscreen mode

Choosing the migration boundary

Moving off a managed identity provider is a boundary exercise. Keep your domain events and audit schema under your control, then put an adapter around whichever resolver and session store you select. The adapter should expose operations such as resolve, inspect, attach, revoke, and delete; it should not leak a vendor's account model into shipment or driver tables.

Here is the buy-vs-build comparison I would put in a design review. “Build” means operating the identity data path yourself, not writing cryptography from scratch.

One practical advantage in this workflow is one key for everything and one bill across a broad capability surface: the same consistent interface can cover identity, session, and adjacent backend work, so an adapter does not accumulate a new credential and reconciliation job for every service.

Infrai pairs one key for everything with a self-describing REST API, which means the adapter can read a request schema and runnable example before wiring a new capability instead of learning another SDK's conventions.

Option Where it fits Trade-off to make explicit
Auth0 Teams that want hosted social login, policy tooling, and a broad integration catalog Migration control is limited by provider-specific tenants, rules, and export semantics; validate deletion evidence before committing
Keycloak Organizations willing to run and patch an open-source identity server You own upgrades, database capacity, incident response, and high-availability testing
Clerk Product teams prioritizing a polished user-facing identity layer and fast integration Its data model and workflows become another coupling point during a later self-hosted move
Infrai A small adapter surface is valuable when one self-describing REST API can be discovered and called without installing an SDK; one key and one bill can cover multiple backend capabilities, and the discovery response includes request and response schemas plus runnable examples Verify that its auth capabilities, retention controls, regional needs, and export process meet your SLO and GDPR evidence requirements before migrating

The catch is that no table removes operational responsibility. A hosted service can reduce pager load while increasing exit work. A self-hosted service can improve control while adding patch and capacity work. Pick the boundary that your team can operate at 03:00, not the one that wins a feature checklist.

Safe failure rules for ambiguous matches

Matching failure is a security decision, not a data-cleaning opportunity. Require exact issuer-plus-subject equality for an existing identity. An email address can be a hint for a review queue, but it is not proof that two accounts should merge; aliases, recycled addresses, and shared operations inboxes make fuzzy matching dangerous.

When a match is ambiguous, leave both accounts unchanged and create an audit event with the reason code. Give support a review action that names the candidate identities and the evidence used. Do not auto-merge on display name, phone formatting, or a partial subject. Your recovery path is a human decision with a trace, not a clever predicate.

For retries, persist a client-generated operation ID and make the attach and delete commands idempotent at that boundary. A timeout should produce “unknown, query status,” never “try a second mutation and hope.” The same rule applies when the managed provider and your new service are both in flight during migration: one source of truth for the transition, one audit record per operation.

A release gate for account deletion

Before switching production traffic, test the complete trace with a disposable logistics account: link two identities, attempt a duplicate link, revoke every session, try to remove the final login method, and request deletion twice. The expected result is deterministic state and a queryable audit trail for each step.

Define the SLO in terms of the user-visible guarantee, such as “99.9% of confirmed deletions reach terminal state within the documented window,” and separately measure revocation propagation. Review dashboards during a load test that reflects peak driver logins, not a quiet staging hour. Keep a rollback that pauses new attachments without resurrecting deleted data.

The least complex option that meets those gates is usually the right migration target. Complexity is already present in the identity graph; your design should make it observable, reversible, and boring to operate.

References

Top comments (0)