refresh.js 2.67 KB
/**
 * 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 }
  }
}