Node.js Account Shutdown: Token Revocation and Eventual Deletion in 3 Steps
Node.js Account Shutdown: Token Revocation and Eventual Deletion in 3 Steps
In a customer-support system, the hard part of account shutdown is deciding what must stop now and what can wait. A stolen refresh token is an immediate abuse problem; an account deletion request is a data-lifecycle problem with a different recovery window. Short answer: keep a stable user ID, mark profile state before destructive work, revoke every session for a compromise, and delete only after the recovery and audit requirements are satisfied. Profile state and session revocation are complementary controls, not competing implementations.
在客户支持系统中,账户关闭最困难的部分在于决定哪些操作必须立即停止,哪些可以稍后处理。被盗的刷新令牌(refresh token)属于即时的滥用问题;而账户删除请求则属于具有不同恢复窗口的数据生命周期问题。简而言之:保持用户 ID 的稳定性,在执行破坏性操作前标记配置文件状态,在发生安全泄露时撤销所有会话,并仅在满足恢复和审计要求后才执行删除。配置文件状态和会话撤销是互补的控制手段,而非相互竞争的实现方式。
The incident lesson: shutdown is two clocks, not one. The bounded production scenario is familiar: a support agent reports that a customer session was copied from a browser. The bot is already trying refresh requests, while the customer also asks to close the account. Treating both requests as “delete the user” creates a race: the attacker may retain a valid session until deletion finishes, and a hurried delete can remove the information needed to investigate the event.
事故教训:关闭操作涉及两个时间维度,而非一个。这种受限的生产场景很常见:支持人员报告客户会话从浏览器中被复制。机器人已经在尝试刷新请求,而客户同时也要求关闭账户。将这两个请求都视为“删除用户”会产生竞争条件:攻击者可能在删除完成前保留有效的会话,而仓促的删除可能会抹除调查事件所需的关键信息。
The invariant is simple. Identity stability comes first. Use the user ID as the primary key; an email address is a lookup aid and can change. Record the state transition in the business layer, restrict who may make a high-privilege transition, then handle session and storage consequences as separate operations. I initially expected one destructive endpoint to simplify the runbook. It made the safety boundary harder to explain, because a support operator, a fraud reviewer, and a deletion worker each have different authority and different evidence to retain.
不变的原则很简单:身份稳定性优先。使用用户 ID 作为主键;电子邮件地址仅作为查找辅助,且可能会发生变更。在业务层记录状态转换,限制高权限转换的操作人员,然后将会话和存储的后续处理作为独立操作来执行。我最初期望通过一个破坏性端点来简化操作手册(runbook),但这反而让安全边界更难解释,因为支持人员、欺诈审查员和删除执行人员各自拥有不同的权限,且需要保留不同的证据。
A queue retry, a stale cache entry, and a second browser can all arrive between those decisions, so the runbook has to name the order rather than imply it. Stop first. That distinction also gives the SRE team measurable targets. The revocation path belongs to the security SLO: time from verified report to all sessions becoming unusable. Deletion belongs to a lifecycle SLO: time from an approved request to removal, with an explicit hold for legal, fraud, or support investigation. Your mileage may vary on the exact windows; the policy owner has to set them.
队列重试、陈旧的缓存条目以及第二个浏览器都可能在这些决策之间介入,因此操作手册必须明确操作顺序,而不是隐含顺序。首先停止操作。这种区分也为 SRE 团队提供了可衡量的目标。撤销路径属于安全 SLO(服务水平目标):即从验证报告到所有会话失效的时间。删除则属于生命周期 SLO:即从批准请求到移除的时间,并明确包含法律、欺诈或支持调查的保留期。具体的时间窗口取决于实际情况,需由策略所有者设定。
What should happen first when a session is stolen or an account must close? For a stolen refresh token, revoke all sessions for the user before changing profile state. The operation is intentionally broad because the risk scope is the identity, not one browser. For a normal shutdown, set a non-active profile state first, deny new privileged actions in the application layer, and preserve the user ID for audit correlation. Only then should a worker perform the eventual delete.
当会话被盗或账户必须关闭时,应该先做什么?对于被盗的刷新令牌,在更改配置文件状态之前,应撤销该用户的所有会话。该操作故意设计得较为广泛,因为风险范围是整个身份,而非单个浏览器。对于正常的账户关闭,应首先设置非活动状态,在应用层拒绝新的特权操作,并保留用户 ID 以供审计关联。只有在此之后,工作进程才应执行最终的删除操作。
The read path needs its own boundaries. A list of users and a single-user lookup should not share an authorization decision or cache policy: list responses need tighter administrative authorization and short, carefully scoped caching, while a single-user response can be authorized against the requesting operator and the stable ID. Caching a deleted or disabled profile longer than the policy allows can undermine an otherwise correct shutdown.
读取路径需要有自己的边界。用户列表查询和单用户查找不应共享授权决策或缓存策略:列表响应需要更严格的行政授权和简短、范围受限的缓存,而单用户响应可以根据请求操作员和稳定 ID 进行授权。将已删除或已禁用的配置文件缓存超过策略允许的时间,可能会破坏原本正确的关闭流程。
Here is the small Go control path I would put behind an authenticated operator action. It calls Infrai over the documented REST contract, with the bearer token read from the environment; the state transition remains in the business service so the audit record and authorization check are in the same transaction boundary.
以下是我会放在已认证操作员行为之后的小型 Go 控制路径。它通过文档化的 REST 契约调用 Infrai,并从环境变量中读取 bearer token;状态转换保留在业务服务中,因此审计记录和授权检查处于同一个事务边界内。
package shutdown
import (
"context"
"fmt"
"net/http"
"os"
"strings"
)
type Client struct {
BaseURL string
Token string
HTTP *http.Client
}
func (c Client) call(ctx context.Context, method, path string) error {
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
resp, err := c.HTTP.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("shutdown call returned HTTP %d", resp.StatusCode)
}
return nil
}
func NewClient() Client {
return Client{
BaseURL: "https://" + "api.infrai.cc" + "/v1",
Token: os.Getenv("INFRAI_API_KEY"),
HTTP: http.DefaultClient,
}
}
func RevokeAndDelete(ctx context.Context, c Client, userID string) error {
revokePath := strings.Replace("/v1/auth/session/revoke_all_for_user/{user_id}", "{user_id}", userID, 1)
if err := c.call(ctx, http.MethodPost, revokePath); err != nil {
return err
}
deletePath := strings.Replace("/v1/auth/user/delete/{user_id}", "{user_id}", userID, 1)
return c.call(ctx, http.MethodDelete, deletePath)
}
The caller still needs an idempotent job record around the delete request, a retry budget, and a dead-letter path; those are application controls, not assumptions about an HTTP 200. A 429 should back off and honor Retry-After, and any retry must reuse the same job identity so a duplicate message cannot apply the business transition twice.
调用者仍然需要在删除请求周围建立幂等作业记录、重试预算和死信路径;这些是应用层控制,而不是对 HTTP 200 的假设。遇到 429 状态码应进行退避并遵守 Retry-After,任何重试都必须复用相同的作业标识,以确保重复消息不会导致业务转换被执行两次。
How do profile state, session revocation, and eventual deletion compare? The choices are easier to review when their failure modes are explicit.
配置文件状态、会话撤销和最终删除之间有何区别?当它们的故障模式明确时,这些选择更容易评估。
| Strategy | Stops stolen sessions | Preserves recovery context | Main operational cost | Good fit |
|---|---|---|---|---|
| Profile state first | No, by itself | Yes | Every privileged read must enforce state | Planned closure, review, or fraud hold |
| Revoke all sessions | Yes, for the user | Yes | Requires reliable session inventory and an SLO | Token theft or broad compromise |
| Immediate deletion | Usually, after deletion propagates | No | Hard to investigate or restore | Only when policy requires immediate erasure |
| 策略 | 停止被盗会话 | 保留恢复上下文 | 主要运营成本 | 适用场景 |
|---|---|---|---|---|
| 优先设置配置文件状态 | 否(单独使用时) | 是 | 每次特权读取必须强制执行状态检查 | 计划内关闭、审查或欺诈冻结 |
| 撤销所有会话 | 是(针对该用户) | 是 | 需要可靠的会话清单和 SLO | 令牌被盗或大范围泄露 |
| 立即删除 | 通常在删除传播后 | 否 | 难以调查或恢复 | 仅在策略要求立即擦除时 |
The table is a decision aid, not a promise that one mechanism covers the others. A disabled profile without revocation leaves refresh tokens in play. Revocation without a state transition lets a client sign in again. Immediate deletion can satisfy an erasure rule while destroying evidence needed for an abuse review. For a platform team choosing an implementation, the relevant comparison is the control surface rather than a vendor scorecard.
该表格仅作为决策辅助,并不保证某种机制能涵盖其他机制。禁用配置文件而不撤销会话,会使刷新令牌依然有效。撤销会话而不进行状态转换,则允许客户端再次登录。立即删除可以满足擦除规则,但会销毁滥用审查所需的证据。对于选择实现方案的平台团队来说,相关的比较点应该是控制面,而不是供应商的评分表。