Repository navigation
feat(login): 连接登录方式 + 新用户邮箱验证页 + 登录 CSRF 防护 - #394
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
配套后端的 /oauth/bind/{provider}。此前设置页只有"解绑",没有绑定入口——
组件注释自己也写着"现在加会把用户登成新账号(分叉)",因为绑定流程还没做。
现在绑定走 /oauth/bind/{provider}(后端要求已登录,把身份挂到当前账号,
不建号、不换会话),所以按钮可以放出来了。
可绑列表来自新端点 GET /api/user-center/identities/providers,返回后端已注册的
provider——前端不再维护"有哪些登录方式"的写死列表,接入新 provider 时按钮会自动
出现。PROVIDER_LABEL 退化成纯展示名,查不到就显示原始 key,保证新 provider 不会
静默消失(顺带用 Object.hasOwn 查表,避免原型链键)。
绑定回调结果 ?bind=ok / ?bind_error=<code> 给出可区分文案,其中 bind_taken
(该第三方账号已绑到别人)必须说清楚"先去那个账号解绑",否则用户只会看到
一个无法自助解决的失败。
用 Suspense 包裹(新增 useSearchParams)。settings 页改动前后均为 ƒ 动态,
非本次引入。72 测试通过 / typecheck / lint 0 error / build 通过。
longsizhuo
force-pushed
the
feat/linked-accounts-connect
branch
from
October 18, 2026 14:15
21021d5 to
74b12fe
Compare
- 连接按钮改为带 satoken header POST /oauth/bind/{provider},拿到 data.url 后
location.assign 跳转;GET 入口后端已下线(GET 可被跨站触发)。401 提示登录
已过期并给出 /login 链接,其它失败显示后端 message 或通用文案。
- ?bind=ok / ?bind_error= 改为渲染时从 URL 派生,不再写进 error state:
load() 成功后 setError(null) 会在约 200ms 后把绑定失败提示冲掉。
- bindTaken 文案不再让用户去另一个账号解绑(后端禁止解绑最后一种登录方式,
照做也走不通),改为说明该登录方式属于另一个账号、合并请联系管理员。
- 导出 labelOf 供新用户邮箱验证页复用。
此前 use-auth 无条件接受任意 /#token=,攻击者发一个带自己 token 的链接就能把
受害者静默登进攻击者账号;配合设置页的"连接",受害者会把自己的第三方身份绑到
攻击者账号上。
- SignInButton 跳 /oauth/render/{provider} 前往 sessionStorage 写一次性 nonce
(发起时间戳,10 分钟有效)。
- AuthProvider 只有在 nonce 存在且未过期时才存 #token,取出即删;否则丢弃
token,仍从 URL 清掉 fragment,继续走已存 token 的正常流程。
- SSO 授权页的 takePending 续接不受影响:它在登录确认之后才执行。
- 新增 tests/login-nonce.test.ts 覆盖接受 / 一次性 / 无 nonce / 过期 / 损坏值。
后端对全新用户(身份、用户名、已验证邮箱都没命中)不再直接建号,而是 302 到
/login/verify 并下发 httpOnly 的 ih_reg_pending cookie,验证完邮箱才创建账号。
- page.tsx:SSG 壳,满足 next-intl 三条件(await params / setRequestLocale 先于
getTranslations / generateStaticParams),robots noindex。
- VerifyEmailForm:loading → email → code → expired。GET /auth/register/pending
预填第三方邮箱并显示 provider;POST send-otp 发码;POST verify 成功后 arm 登录
nonce 再 location.replace('/{locale}#token=…'),复用 use-auth 的 #token 流程
(含 SSO 续接)。email / code 两步始终提示"已有账号请用原方式登录后在设置里连接 {provider}",
这是邮箱为空的老账号防分叉的唯一手段。
- 导出纯函数 resolveVerifyError 把 400/409/410/429/503 映射到 i18n key,
tests/verify-email.test.ts 覆盖映射、key 存在性和 zh/en key 集合一致。
- 新增 zh/en verifyEmail 命名空间。
- LinkedAccounts 的 /login 链接改用 next/link:@/i18n/navigation 在 vitest
node 环境下无法解析,而验证页要复用它导出的 labelOf。
后端 OAuth 回调遇到被停用的账号会 302 到 /login?error=account_disabled, 此前会落到通用的"请重试",而重试永远不会成功。新增 zh/en 文案提示账号已停用、 联系管理员;映射仍走显式白名单 + Object.hasOwn。
改用 next/link 后 href="/login" 不带 locale 前缀,要靠 middleware 再按 cookie /
Accept-Language 猜一次语言并 307:英文页面上的用户可能被送到 /zh/login。
直接拼 /{locale}/login,和当前页面语言保持一致,也省一次跳转。
后端在 409(邮箱已被占用)前已消费掉注册会话,之后"验证"/"重发"/"换邮箱" 只会拿到 410,并把"该邮箱已有账号"的提示换成错误的"会话已过期"。409 改为 与 410 一样切到 expired 步骤,保留 errorEmailTaken 文案和返回登录链接。
时间戳 nonce 只能证明"本标签页 10 分钟内发起过登录":登录未带回 #token
(报错、新用户验证页、授权页停留)时 nonce 一直有效,持有窗口句柄的打开者
可在此期间把标签页导向 /#token=<攻击者 token> 完成 login CSRF。
- SignInButton 生成 16 字节随机 nonce(base64url)存 sessionStorage,
跳 /oauth/render/{provider}?n=<nonce>;后端登录回调回跳 #token=…&n=…。
- AuthProvider 只在 n 与存的 nonce 完全一致时接受 token;只要带 #token 就
消费掉已存 nonce,并从 URL 清掉 token 和 n。
- 被拒时 location.replace 到 /<locale>/login?error=oauth_state,而不是静默
以未登录继续;不取 SSO pending,重新登录成功后照常续接授权请求。
- 新用户验证页验证成功后同样生成 nonce,带 &n= 跳回。
- 测试覆盖:完全一致才接受 / 缺失或不一致拒绝 / 任何一次校验都消费 nonce /
格式与长度 / 被拒后 SSO pending 仍在。
需先部署后端(回传 n),再部署前端。
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
配套 backend InvolutionHell/involutionhell-backend#57。必须在后端上线之后再合并:拿不到后端回传的
n,前端会拒收所有#token。内容
POST /oauth/bind/{provider}取授权地址再跳转;401 时提示重新登录。?bind=ok和?bind_error=的结果单独存一份状态,不会再被列表刷新冲掉。bind_taken的文案改成"该登录方式属于另一个账号,请联系管理员合并"。/[locale]/login/verify:按 SSG 渲染,noindex。流程是 pending → 发码 → 验证 → 登入。409 和 410 都进入终态,并始终提示"已有账号请用原方式登录后到设置里连接"。/oauth/render/{p}?n=。AuthProvider 只接受n与本地 nonce 完全一致的#token;被拒时跳/login?error=oauth_state,挂起的 SSO 请求保留,重新登录后继续。验证页成功后也走同一条路。account_disabled。验证
pnpm typecheck、pnpm lint(0 error)、pnpm test(132 个全部通过)、pnpm build全部通过。/[locale]/login、/[locale]/login/verify、/[locale]/sso/authorize均为 ●(SSG)。🤖 Generated with Claude Code