refresh.js
2.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 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 }
}
}