/** * refresh 轮转 + token-reuse 检测(本包最关键的安全路径)。 * * 正常轮转:旧 refresh 校验通过 → 立即标记 used → 沿用同一 family 签发新 access+refresh。 * Token-reuse 检测:若提交的 refresh 对应会话已被标记 used(重放),视为凭据泄露, * 抛 REFRESH_REUSED 并撤销整个会话族(family)全部 token——后续该族任何 refresh 全失效。 * * 密码学(refresh 比对)走注入 hasher,签发走 issueSession,本文件只做状态机决策。 */ import { AuthError } from './errors.js' import { issueSession, parseRefreshToken } from './session.js' /** * @param {Object} deps * @param {import('./index.js').UserRepo} deps.userRepo * @param {import('./index.js').TokenSigner} deps.tokenSigner * @param {import('./index.js').Hasher} deps.hasher * @param {number} [deps.accessTtlMs] * @param {number} [deps.refreshTtlMs] * @returns {(args: { refresh_token: string }) => Promise<{ user: import('./index.js').User|{id:string}, access_token: string, refresh_token: string }>} */ export function makeRefresh(deps) { const { userRepo, hasher } = deps return async function refresh({ refresh_token }) { const parsed = parseRefreshToken(refresh_token) if (!parsed) throw new AuthError({ code: 'TOKEN_INVALID' }) const session = await userRepo.findSession({ sid: parsed.sid }) if (!session || session.revoked) { throw new AuthError({ code: 'TOKEN_INVALID' }) } // 校验提交的 refresh 与落库 hash 匹配(防伪造 sid 重放)。 const matches = await hasher.verify(parsed.secret, session.refresh_hash) if (!matches) { throw new AuthError({ code: 'TOKEN_INVALID' }) } // Token-reuse:该会话的 refresh 已被用过却又被提交 → 凭据泄露,撤销整个会话族。 if (session.used) { await userRepo.revokeFamily({ family: session.family }) throw new AuthError({ code: 'REFRESH_REUSED' }) } if (Date.now() > session.expires_at.getTime()) { throw new AuthError({ code: 'TOKEN_EXPIRED' }) } // 正常轮转:旧 refresh 立即失效,沿用 family 签发新对。 await userRepo.markRefreshUsed({ sid: session.sid }) const issued = await issueSession(deps, { user_id: session.user_id, family: session.family }) // 带回 user(前端 SK 门面映射 account 需要);findById 不可用时优雅降级为 { id }。 let user = { id: session.user_id } if (typeof userRepo.findById === 'function') { const found = await userRepo.findById(session.user_id) if (found) user = found } return { user, access_token: issued.access_token, refresh_token: issued.refresh_token } } }