7.2 KiB
Auth Login Methods(登录方式接入契约)
面板现有登录方式:密码(+可选 TOTP)、OIDC / GitHub(OAuth)、通行密钥(Passkey / WebAuthn)、Web3 钱包(EIP-4361)。 新增或修改任何登录方式时,以下契约是安全红线,缺一不可。
1. 登录守卫(防爆破)
公开登录端点的失败必须计入 loginGuard:guardKey(clientIP, <账号标识>),账号未知用固定占位名(如 passkey 的 __passkey__),已知用其标识(如钱包地址小写)。锁定检查在校验之前;失败经 auth.failLogin(自带阈值转锁定与告警推送),成功 guard.success。阈值/时长取安全设置,不硬编码。
2. 系统日志留痕
公开登录端点不经系统日志中间件,handler 必须显式 logs.Record 成败(参考 authHandler.recordLogin / passkeyHandler.recordLogin / walletHandler.recordWallet);登录失败是安全关键事件,不允许无痕。JWT 组内的绑定/解绑由中间件自动留痕,无需额外埋点。
3. 令牌版本(TokenVersion)
绑定、解绑、凭据及认证因子变更均属敏感变更:必须在事务内
bumpTokenVersionTx(或 RevokeSessions)使旧 JWT 失效,并按第 6 节携带
TokenProof、接续操作者会话。因子写入与版本递增不得拆成两个独立提交。
4. 「至少一种登录方式」不变量
密码登录禁用要求至少一种在当前 EffectiveAppURL 下可实际登录的免密方式
(usablePasswordlessTx);地址为空时 Passkey、钱包和 OAuth 均不可用。禁用期间移除
最后一种免密方式被拒:解绑身份走 ensureNotLastLogin,删通行密钥走对称检查。
这些检查都在事务内先 lockUserForAuthChange 锁用户行,防并发绕过。新登录方式要
明确决策如何参与该不变量:
- 复用
UserIdentity表(如钱包,provider+subject 唯一)→ 走identityCountTx自动参与,零改动; - 独立表(如 Passkey 的
UserPasskey)→ 参照passkeyCountTx增加计数入口,并在开启门槛(hasAnyPasswordless)与两个ensureNot*检查里对称接入;api 层把ErrLastIdentity映射 409。
5. 挑战/状态的一次性消费
挑战-响应类流程(OAuth state、WebAuthn challenge、SIWE nonce)一律进程内 map + mutex + TTL + gc,取用即删(参考 oauthPending / passkeyPending / walletPending);不落库、不引 Redis。TTL 5-10 分钟。
6. 场景:已鉴权敏感事务的提交屏障
1. Scope / Trigger
凡请求先通过 JWT 中间件、随后仍会读取 body、等待外部回调或写入认证/安全配置, 都必须防止请求处理期间的 Logout、定点撤销或撤销全部会话被旧请求绕过。
2. Signatures
type TokenProof struct {
Ver uint
Jti string
}
func tokenProofOf(c *gin.Context) service.TokenProof
func (s *AuthService) ensureTokenCurrentTx(
tx *gorm.DB, user *model.User, proof TokenProof,
) error
func (s *AuthService) RenewToken(
ctx context.Context, username, oldToken string, meta SessionMeta,
) (string, time.Time, error)
持久化契约使用 user_sessions.token_id 表示逻辑会话;敏感换发不改变该 JTI。
3. Contracts
RequireAuth必须把本次请求实际观察到的TokenVersion + JTI写入 Context, Handler 不得事后只按用户名读取“最新版本”代替 proof。- 敏感事务、
RevokeSession与 Logout 的数据库路径统一先锁用户行,再检查/更新 会话行;持锁后调用ensureTokenCurrentTx。 - proof 通过后,认证因子写入、版本递增及事务内换发必须同成同败。需要在 Handler
末尾换发的旧接口,
RenewToken也须重新锁用户并确认当前版本恰为oldClaims.Ver + 1。 - 换发沿用旧 JWT 的合法 JTI;有旧会话行时只接续未撤销行,存在但已撤销或更新 失败必须关闭。兼容期无行 Token 可建行,但仍沿用旧 JTI,因此 Logout 的 tombstone 能同时撤销换发前后的 JWT。
- Logout 持久撤销须按 JTI 会话行解析稳定的
user_id,不得只按旧 JWT 的可变 username/subject 找用户;JTI tombstone 至少保留“旧 JWT 剩余寿命 + 一个完整tokenTTL”,兜住 legacy 无行换发、改名提交和持久化撤销之间的 窄窗口。 - 外部身份登录须在用户锁内重查身份仍绑定;OAuth 登录还须在最终签发事务内重查 Provider 当前可用。
- proof 不二次检查自然到期:请求在 JWT 未过期时通过中间件即取得本次请求的认证 边界;不得为此额外引入长事务、分布式锁或持久状态机。
4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
user.TokenVersion != proof.Ver |
ErrTokenStale → HTTP 401 |
| JTI tombstone 命中或会话行已撤销 | ErrTokenStale → HTTP 401 |
| 旧会话行存在但接续命中 0 行 | 失败关闭,不得 fallback 创建 |
| 已验签当前版本的兼容 Token 确无会话行 | 沿用原 JTI 创建会话行 |
| 改名后用换发前 JWT Logout | 按 JTI/user_id 撤销新旧 JWT;旧 subject 不参与归属 |
Handler 分段换发时版本不是 oldVer + 1 |
ErrTokenStale,前端重新登录 |
| 登录签发前身份已解绑 / Provider 已不可用 | 拒绝登录,不签发 JWT |
5. Good / Base / Bad Cases
- Good:TOTP 激活事务内依次锁用户、验 proof、写因子、递增版本、沿用 JTI 换发。
- Base:无会话行的存量 JWT 可完成一次合法变更,但换发后的 JWT 与旧 JWT 共用 JTI,后到 Logout 可同时撤销。
- Bad:proof 普通查询通过后释放锁,定点撤销完成,再把旧行更新失败当“无行”创建 新会话;这会复活已撤销 Token。
6. Tests Required
- TOTP 激活 × 撤销全部:旧 proof 返回
ErrTokenStale,因子与版本均不改变。 - proof check → 定点撤销 → renew:撤销后的行不得被新建替代。
- recorded 与 legacy 无行会话各测一次“改名后 old-token Logout”:换发 JWT
必须随同失效;recorded 行写入
revoked_at,legacy tombstone 覆盖 fresh expiry。 - OAuth/钱包登录在最终事务前删除身份:不得签发会话;OAuth 禁用同理。
- Security/OAuth PATCH 使用已撤销 proof:返回
ErrTokenStale且设置不落库。
7. Wrong vs Correct
// Wrong:只按最新用户状态改因子,随后无条件签最新版本 JWT。
saveFactor()
bumpTokenVersion()
return IssueToken(ctx, username)
// Correct:请求 proof、因子写入、版本递增和会话接续共享事务与用户锁。
return db.Transaction(func(tx *gorm.DB) error {
user, err := lockUserForAuthChange(tx, username)
if err != nil { return err }
if err := auth.ensureTokenCurrentTx(tx, user, proof); err != nil { return err }
return changeFactorAndRenewTx(tx, user, oldToken)
})
其他约定
- 面板地址依赖:RP ID / 回调 / SIWE domain 均派生自
settings.EffectiveAppURL(),为空时返回引导错误(文案对齐ErrOAuthNoAppURL),不得回退猜测。 - 登录失败对外文案统一、不透出内部细节(防探测);错误变量集中定义在对应 service 文件头部。
- 新端点全量 swagger 中文注释并重新生成 docs/;providers 公开端点(
/auth/oauth/providers)按需追加<method>Login可用性布尔,供登录页渲染入口。