oauth.js
2.92 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
/**
* OAuth 登录:经注入 oauth Port 换码 + 验签取邮箱 → 首登建号 / 已存在合并 → 签发会话。
*
* 红线:包内不自写 JWKS 拉取 / RS256 验签 / id_token 解码——全在 oauth.exchange 内完成
* (Google 走 google-auth-library 校验 id_token、Apple 走其 JWKS)。验签失败统一
* OAUTH_VERIFY_FAILED。本文件只做 provider 校验 + 建号/合并的账户状态机。
*/
import { AuthError } from './errors.js'
import { issueSession } from './session.js'
const SUPPORTED_PROVIDERS = new Set(['google', 'apple'])
/**
* @param {Object} deps
* @param {import('./index.js').UserRepo} deps.userRepo
* @param {import('./index.js').OauthPort} deps.oauth
* @param {import('./index.js').TokenSigner} deps.tokenSigner
* @param {import('./index.js').Hasher} deps.hasher
* @param {number} [deps.accessTtlMs]
* @param {number} [deps.refreshTtlMs]
* @returns {(args: { provider: string, code: string, oauth_flow?: 'access_token'|'authorization_code', redirect_uri?: string, device_id?: string }) => Promise<import('./index.js').AuthTokens & { is_new_user: boolean }>}
*/
export function makeOauthLogin(deps) {
const { userRepo, oauth } = deps
return async function oauthLogin({ provider, code, oauth_flow = 'access_token', redirect_uri }) {
if (!SUPPORTED_PROVIDERS.has(provider)) {
throw new AuthError({ code: 'UNSUPPORTED_PROVIDER', message: `provider: ${provider}` })
}
let identity
try {
identity = await oauth.exchange({
provider: /** @type {'google'|'apple'} */ (provider),
code,
oauth_flow,
redirect_uri,
})
} catch (err) {
throw new AuthError({ code: 'OAUTH_VERIFY_FAILED', cause: err })
}
if (!identity || !identity.email || !identity.provider_user_id) {
throw new AuthError({ code: 'OAUTH_VERIFY_FAILED' })
}
const email = String(identity.email).trim().toLowerCase()
let user = await userRepo.findByEmail(email)
let is_new_user = false
if (!user) {
// 首登:自动建号并绑定 provider 身份。
user = await userRepo.create({
email,
oauth_providers: [{ provider, provider_user_id: identity.provider_user_id }],
...(identity.email_verified ? { email_verified_at: new Date() } : {}),
})
is_new_user = true
} else {
// 已存在:合并到同一 user(绑定该 provider,若未绑)。
const linked = (user.oauth_providers ?? []).some(
(p) => p.provider === provider && p.provider_user_id === identity.provider_user_id,
)
if (!linked) {
user = await userRepo.linkOauth({
user_id: user.id,
provider,
provider_user_id: identity.provider_user_id,
})
}
}
const issued = await issueSession(deps, { user_id: user.id })
return {
user,
access_token: issued.access_token,
refresh_token: issued.refresh_token,
is_new_user,
}
}
}