Skip to content

feat: sign auth cookies with expiry, opt-in login rate limiting, login UX fixes - #221

Open
libertydragonn wants to merge 4 commits into
Decohererk:mainfrom
libertydragonn:redo/auth-cookie-ratelimit
Open

feat: sign auth cookies with expiry, opt-in login rate limiting, login UX fixes#221
libertydragonn wants to merge 4 commits into
Decohererk:mainfrom
libertydragonn:redo/auth-cookie-ratelimit

Conversation

@libertydragonn

Copy link
Copy Markdown
Contributor

背景

重做系列的核心 auth 加固,依赖 #217(密码哈希),在其合并前 diff 会包含那一支的 commit。这个 PR 同时修复了上次合并→回退过程中「登录页状态异常 / 正确密码报错」的三个根因(见下)。

上次登录回归的根因分析(本地已复现并验证修复)

  1. 限速把整站锁死:上一版限速把无法识别 IP 的请求全部归入同一个桶。Docker/反代部署下所有请求看到的都是同一个网关 IP,几次失败尝试(包括升级后旧会话产生的失败)就会让所有人收到 429——而登录页把所有非 200 显示成「密码错误」。本 PR 把限速改为默认关闭LOGIN_RATE_LIMIT=true 显式开启,且仅在反代转发真实客户端 IP 时建议开启);开启时按客户端 IP 分桶,被限的客户端永远不会连累站长。
  2. 登录页错误提示误导:现在 429 显示等待时间、401 显示服务器返回的具体原因,不再一律「密码错误」。
  3. 登录页残留已登录 UI:升级后残留的可读 cookie 会让 /login 页渲染出已登录的导航栏(「像是卡在登录态判断中」)。NavbarGate 现在在 /login、/register 一律隐藏导航栏。

改动

  • 新增共享 auth-signature 模块:HMAC 签名内容为 username:role:iat:exp,优先使用独立 AUTH_SECRETNEXTAUTH_SECRET 次之,未配置回退 PASSWORD,零配置升级)。过期或被篡改的 cookie 服务端直接拒绝;轮换密钥可一次性注销所有会话。
  • auth cookie 改为 HttpOnly,新增可读的 auth_meta(仅 username/role)供前端展示。
  • verifyApiAuth 与 middleware 验证签名过期;旧格式 cookie 会被拒绝——升级后所有用户需重新登录一次(一次性,验证过恢复流程正常)。
  • 登录失败限速:默认关闭;开启后按「客户端IP+账号」5 次 / 15 分钟、「客户端IP」15 次 / 15 分钟,成功登录清零,429 带 Retry-After
  • README 记录 AUTH_SECRETLOGIN_RATE_LIMIT

验证(真实 Docker 部署,kvrocks)

用本分支镜像 docker build + docker run(kvrocks compose 等价配置)实测:

  • 站长登录 200 / 错误密码 401;登录后 /api/favorites 等受保护 API 200
  • Set-Cookie: auth=...; HttpOnly; SameSite=laxauth_meta 可读
  • 预置明文密码用户登录 → 200 并迁移为 scrypt;二次登录 200
  • 默认(未开启限速)连续 12 次错误密码后,正确密码仍然 200——上次的锁死回归不再存在
  • 开启 LOGIN_RATE_LIMIT=true 时:同一转发 IP 5 次失败后 429(含 Retry-After),换一个客户端 IP 用正确密码立即 200(站长不会被攻击者连累)
  • 伪造/篡改 exp 的 cookie 被 middleware 拒绝(307 → /login)
  • 旧 cookie → 强制重登 → 重新登录后一切正常;登出再登录、刷新页面均正常
  • jest --runInBand 22 套件 / 122 测试通过;pnpm typecheck 通过

升级须知

合并部署后,所有已登录用户的旧 cookie 失效,需要重新登录一次(预期行为,登录页会正常引导)。建议同时设置 AUTH_SECRETopenssl rand -base64 32)。

- Add src/lib/password.ts: scrypt hashing (N=16384,r=8,p=1), constant-time
  verification, strict stored-hash detection, and normalize-on-write.
- redis/kvrocks, upstash, and memory stores now hash on register/change
  and re-hash legacy plaintext on the next successful login.
- Add password storage format tests.

Migration note: verifying a legacy plaintext password rewrites it to a
scrypt hash in place. This is a one-way upgrade — if the deployment is
later rolled back to a build that compares plaintext, those already-
migrated users can no longer log in until their password is reset. See
the PR description for details.
…x login UX

Auth cookie signing:
- Shared auth-signature module signs username:role:iat:exp, preferring a
  dedicated AUTH_SECRET (NEXTAUTH_SECRET fallback) over PASSWORD; expired
  or tampered cookies are rejected server-side and rotating the secret
  invalidates all sessions.
- verifyApiAuth and middleware verify the signed expiry; legacy cookies
  without iat/exp are rejected (one-time re-login after deploy).
- Split the readable auth_meta cookie from the HttpOnly auth cookie.

Login rate limiting (hardened against self-lockout):
- Per client-IP+account (5/15min) and per-IP (15/15min) only. Buckets are
  keyed on the real client IP so a limited client can never lock out the
  operator on the same account.
- When no client IP is forwarded (direct LAN/Docker exposure) rate
  limiting is disabled for that request instead of collapsing every client
  into one bucket — this avoids the 429 lockout that looked like a broken
  login in the earlier merge.

Login UX fixes (root causes of the earlier "login stuck / wrong password"
reports):
- Login page now distinguishes 429 (too many attempts, shows wait time)
  and surfaces the server's specific 401 reason instead of always saying
  the password is wrong.
- NavbarGate always hides the navbar on /login and /register; it no longer
  renders a logged-in navbar when a stale readable cookie lingers after an
  upgrade.
- await all now-async verifyApiAuth callers.

Depends on the password-hashing PR. Verified with a real Docker kvrocks
deployment: owner and member login, legacy-cookie re-login, logout and
re-login, and page refresh all behave correctly.
Behind Docker/reverse proxies every request is observed with the same
gateway/proxy IP, so an always-on limiter collapses all clients into one
bucket and a few failed attempts (including those a stale post-upgrade
session produces) lock the whole instance out with 429s — a self-inflicted
login outage. Rate limiting is now disabled unless LOGIN_RATE_LIMIT=true,
so login can never be throttled out of the box; operators whose proxy
forwards the real client IP can opt in. When enabled, buckets stay keyed on
the client IP so a limited client cannot lock out the operator.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant