auth.js
5.21 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
* createAuth —— 唯一入口工厂,只做依赖装配 + 各能力编排。
*
* 纪律:本文件不碰任何密码学(全在注入 Port);缺依赖在装配时即 assert 抛错
* (不拖到运行某行);每条业务流程在独立文件(register/login/oauth/refresh/code)。
* verifyAccessToken / logout / revokeAllSessions 是薄方法,直接落本文件。
*/
import { AuthError } from './errors.js'
import { makeRegister } from './register.js'
import { makeLogin } from './login.js'
import { makeOauthLogin } from './oauth.js'
import { makeRefresh } from './refresh.js'
import { makeSendCode, makeVerifyCode } from './code.js'
import { makeResetPassword } from './reset-password.js'
import { makeRequestPasswordReset } from './request-password-reset.js'
import { parseRefreshToken } from './session.js'
/**
* @param {Object} [deps]
* @param {import('./index.js').UserRepo} [deps.userRepo]
* @param {import('./index.js').EmailPort} [deps.email]
* @param {import('./index.js').OauthPort} [deps.oauth]
* @param {import('./index.js').TokenSigner} [deps.tokenSigner]
* @param {import('./index.js').Hasher} [deps.hasher]
* @param {Object} [opts] 可选的业务规则覆写(TTL / 阈值 / 窗口)。
* @returns {import('./index.js').Auth}
*/
export function createAuth({ userRepo, email, oauth, tokenSigner, hasher } = {}, opts = {}) {
assertDep('userRepo', userRepo, ['findByEmail', 'create', 'updatePasswordHash', 'saveSession', 'findSession'])
assertDep('email', email, ['sendCode'])
assertDep('oauth', oauth, ['exchange'])
assertDep('tokenSigner', tokenSigner, ['sign', 'verify'])
assertDep('hasher', hasher, ['hash', 'verify'])
const deps = /** @type {{ userRepo: import('./index.js').UserRepo, email: import('./index.js').EmailPort, oauth: import('./index.js').OauthPort, tokenSigner: import('./index.js').TokenSigner, hasher: import('./index.js').Hasher, maxAttempts?: number, windowMs?: number, accessTtlMs?: number, refreshTtlMs?: number, ttlMs?: number, rand?: () => number }} */ (
{ userRepo, email, oauth, tokenSigner, hasher, ...opts }
)
const register = makeRegister(deps)
const login = makeLogin(deps)
const oauthLogin = makeOauthLogin(deps)
const refresh = makeRefresh(deps)
const sendCode = makeSendCode(deps)
const requestPasswordReset = makeRequestPasswordReset({ userRepo: deps.userRepo, sendCode })
const verifyCode = makeVerifyCode(deps)
const resetPassword = makeResetPassword(deps)
/** @type {import('./index.js').Auth['verifyAccessToken']} */
async function verifyAccessToken(token) {
if (!token || typeof token !== 'string') {
throw new AuthError({ code: 'TOKEN_INVALID' })
}
try {
const claims = await deps.tokenSigner.verify(token)
await assertAccessSessionActive(claims)
return claims
} catch (/** @type {any} */ err) {
if (err instanceof AuthError) throw err
// jose 用 'ERR_JWT_EXPIRED'(code 或 name)区分过期与无效签名。
const tag = (err && (err.code || err.name)) || ''
const expired = /EXPIRED/i.test(String(tag))
throw new AuthError({ code: expired ? 'TOKEN_EXPIRED' : 'TOKEN_INVALID', cause: err })
}
}
/**
* @param {Record<string, any>} claims
*/
async function assertAccessSessionActive(claims) {
const sid = String(claims?.sid || '')
if (!sid) throw new AuthError({ code: 'TOKEN_INVALID' })
const session = await deps.userRepo.findSession({ sid })
if (!session || session.revoked) throw new AuthError({ code: 'TOKEN_INVALID' })
if (Date.now() > session.expires_at.getTime()) {
throw new AuthError({ code: 'TOKEN_EXPIRED' })
}
}
/** @type {import('./index.js').Auth['logout']} */
async function logout({ refresh_token }) {
const parsed = parseRefreshToken(refresh_token)
if (parsed) {
const session = await deps.userRepo.findSession({ sid: parsed.sid })
if (session) await deps.userRepo.revokeFamily({ family: session.family })
}
return { ok: true }
}
/** @type {import('./index.js').Auth['revokeAllSessions']} */
async function revokeAllSessions({ user_id }) {
if (!user_id) throw new AuthError({ code: 'USER_NOT_FOUND' })
await deps.userRepo.revokeUserSessions({ user_id })
return { ok: true }
}
/**
* @param {{ user_id: string }} args
*/
async function getUser({ user_id }) {
if (!user_id || typeof deps.userRepo.findById !== 'function') return null
return deps.userRepo.findById(user_id)
}
return {
register,
login,
oauthLogin,
refresh,
verifyCode,
sendCode,
requestPasswordReset,
resetPassword,
getUser,
verifyAccessToken,
logout,
revokeAllSessions,
}
}
/**
* 装配时依赖自检:缺整个依赖或缺关键方法都立刻抛 AuthError。
* @param {string} name
* @param {any} dep
* @param {string[]} methods
*/
function assertDep(name, dep, methods) {
if (!dep || typeof dep !== 'object') {
throw new AuthError({ code: 'TOKEN_INVALID', message: `createAuth: missing dependency "${name}"` })
}
for (const m of methods) {
if (typeof dep[m] !== 'function') {
throw new AuthError({
code: 'TOKEN_INVALID',
message: `createAuth: dependency "${name}" missing method "${m}"`,
})
}
}
}