import assert from "assert"; import { OakUserException } from "oak-domain/lib/types"; import { generateNewIdAsync } from "oak-domain/lib/utils/uuid"; import { loadTokenInfo, setUpTokenAndUser } from "./token"; import { randomUUID } from "crypto"; import { processUserInfo } from "../utils/oauth"; export async function loginByOauth(params, context) { const { code, state: stateCode, env } = params; const closeRootMode = context.openRootMode(); const currentUserId = context.getCurrentUserId(true); const applicationId = context.getApplicationId(); const islogginedIn = !!currentUserId; assert(applicationId, '无法获取当前应用ID'); assert(code, 'code 参数缺失'); assert(stateCode, 'state 参数缺失'); // 验证 state 并获取 OAuth 配置 const [state] = await context.select("oauthState", { data: { providerId: 1, provider: { type: 1, clientId: 1, redirectUri: 1, clientSecret: 1, tokenEndpoint: 1, userInfoEndpoint: 1, ableState: 1, autoRegister: 1, }, usedAt: 1, }, filter: { state: stateCode, }, }, { dontCollect: true }); const systemId = context.getSystemId(); const [applicationPassport] = await context.select('applicationPassport', { data: { id: 1, applicationId: 1, passportId: 1, passport: { id: 1, type: 1, systemId: 1, config: 1, }, }, filter: { passport: { systemId, type: 'oauth', }, applicationId, } }, { dontCollect: true }); const allowOauth = !!(state.providerId && applicationPassport?.passport?.config?.oauthIds && applicationPassport?.passport?.config)?.oauthIds.includes(state.providerId); if (!allowOauth) { throw new OakUserException('error::user.loginWayDisabled'); } assert(state, '无效的 state 参数'); assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用'); // 如果已经使用 if (state.usedAt) { throw new OakUserException('该授权请求已被使用,请重新发起授权请求'); } // 更新为使用过 await context.operate("oauthState", { id: await generateNewIdAsync(), action: 'update', data: { usedAt: Date.now(), }, filter: { id: state.id, } }, { dontCollect: true }); // 使用 code 换取 access_token 并获取用户信息 const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(code, state.provider); const [existingOAuthUser] = await context.select("oauthUser", { data: { id: 1, userId: 1, providerUserId: 1, user: { id: 1, userState: 1, refId: 1, ref: { id: 1, userState: 1, } } }, filter: { providerUserId: oauthUserInfo.providerUserId, providerConfigId: state.providerId, } }, { dontCollect: true }); // 已登录的情况 if (islogginedIn) { // 检查当前用户是否已绑定此提供商 const [currentUserBinding] = await context.select("oauthUser", { data: { id: 1, }, filter: { userId: currentUserId, providerConfigId: state.providerId, } }, {}); if (currentUserBinding) { throw new OakUserException('当前用户已绑定该 OAuth 平台账号'); } if (existingOAuthUser) { throw new OakUserException('该 OAuth 账号已被其他用户绑定'); } console.log("绑定 OAuth 账号到当前用户:", currentUserId, oauthUserInfo.providerUserId); // 创建绑定关系 await context.operate("oauthUser", { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), userId: currentUserId, providerConfigId: state.providerId, providerUserId: oauthUserInfo.providerUserId, rawUserInfo: oauthUserInfo.rawData, accessToken, accessExpiresAt: accessTokenExp, refreshToken, refreshExpiresAt: refreshTokenExp, applicationId, stateId: state.id, } }, { dontCollect: true }); // 返回当前 token const tokenValue = context.getTokenValue(); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; // 未登录,OAuth账号已存在,直接登录 } else if (existingOAuthUser) { console.log("使用已绑定的 OAuth 账号登录:", existingOAuthUser.id); const { user } = existingOAuthUser; const targetUser = user?.userState === 'merged' ? user.ref : user; const tokenValue = await setUpTokenAndUser(env, context, 'oauthUser', existingOAuthUser.id, // 使用已存在的 oauthUser ID undefined, targetUser // 关联的用户 ); // 更新登录信息 await context.operate("oauthUser", { id: await generateNewIdAsync(), action: 'update', data: { rawUserInfo: oauthUserInfo.rawData, accessToken, accessExpiresAt: accessTokenExp, refreshToken, refreshExpiresAt: refreshTokenExp, applicationId, stateId: state.id, }, filter: { id: existingOAuthUser.id, } }, { dontCollect: true }); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } // 未登录,OAuth账号不存在,创建新用户 else { if (!state.provider.autoRegister) { throw new OakUserException('您还没有账号,请先注册一个账号'); } console.log("使用未绑定的 OAuth 账号登录:", oauthUserInfo.providerUserId); const newUserId = await generateNewIdAsync(); const oauthUserCreateData = { id: newUserId, providerConfigId: state.providerId, providerUserId: oauthUserInfo.providerUserId, rawUserInfo: oauthUserInfo.rawData, accessToken, accessExpiresAt: accessTokenExp, refreshToken, refreshExpiresAt: refreshTokenExp, applicationId, stateId: state.id, loadState: 'unload' }; // 不传 user 参数,会自动创建新用户 const tokenValue = await setUpTokenAndUser(env, context, 'oauthUser', undefined, oauthUserCreateData, // 创建新的 oauthUser undefined // 不传 user,自动创建新用户 ); await context.operate("oauthUser", { id: await generateNewIdAsync(), action: 'loadUserInfo', data: {}, filter: { id: newUserId, } }, { dontCollect: true }); await loadTokenInfo(tokenValue, context); closeRootMode(); return tokenValue; } } export async function getOAuthClientInfo(params, context) { const { client_id, currentUserId } = params; const closeRootMode = context.openRootMode(); const systemId = context.getSystemId(); const applicationId = context.getApplicationId(); const [oauthApp] = await context.select("oauthApplication", { data: { id: 1, name: 1, redirectUris: 1, description: 1, logo: 1, isConfidential: 1, }, filter: { id: client_id, systemId: systemId, ableState: "enabled", } }, { dontCollect: true }); // 如果还有正在生效的授权,说明已经授权过了 const [hasAuth] = await context.select("oauthUserAuthorization", { data: { id: 1, userId: 1, applicationId: 1, usageState: 1, authorizedAt: 1, }, filter: { // 如果 已经授权过token并且 没有被撤销 tokenId: { $exists: true }, token: { revokedAt: { $exists: false } }, usageState: 'granted', code: { // 当前应用下的认证客户端 oauthApp: { id: client_id }, applicationId: applicationId, userId: currentUserId, } } }, { dontCollect: true }); if (hasAuth) { console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState); } if (!oauthApp) { throw new OakUserException('未经授权的客户端应用'); } closeRootMode(); return { data: oauthApp, alreadyAuth: !!hasAuth, }; } export async function createOAuthState(params, context) { const { providerId, userId, type } = params; const closeRootMode = context.openRootMode(); const generateCode = () => { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); }; const state = generateCode(); await context.operate("oauthState", { id: await generateNewIdAsync(), action: 'create', data: { id: await generateNewIdAsync(), providerId, userId, type, state } }, { dontCollect: true }); closeRootMode(); return state; } export async function authorize(params, context) { const { response_type, client_id, redirect_uri, scope, state, action, code_challenge, code_challenge_method } = params; if (response_type !== 'code') { throw new OakUserException('不支持的 response_type 类型'); } const closeRootMode = context.openRootMode(); const systemId = context.getSystemId(); const [oauthApp] = await context.select("oauthApplication", { data: { id: 1, redirectUris: 1, isConfidential: 1, }, filter: { id: client_id, systemId: systemId, } }, { dontCollect: true }); if (!oauthApp) { throw new OakUserException('未经授权的客户端应用'); } // 创建授权记录 const recordId = await generateNewIdAsync(); await context.operate("oauthUserAuthorization", { id: await generateNewIdAsync(), action: 'create', data: { id: recordId, userId: context.getCurrentUserId(), applicationId: oauthApp.id, usageState: action === 'grant' ? 'unused' : 'denied', authorizedAt: Date.now(), } }, { dontCollect: true }); if (action === 'deny') { const params = new URLSearchParams(); params.set('error', 'access_denied'); params.set('error_description', '用户拒绝了授权请求'); if (state) { params.set('state', state); } closeRootMode(); return { redirectUri: `${redirect_uri}?${params.toString()}`, }; } if (action === 'grant') { // 检查redirectUri 是否在注册的列表中 if (!oauthApp.redirectUris?.includes(redirect_uri)) { console.log('不合法的重定向 URI:', redirect_uri, oauthApp.redirectUris); throw new OakUserException('重定向 URI 不合法'); } const code = randomUUID(); const codeId = await generateNewIdAsync(); // 存储授权码 await context.operate("oauthAuthorizationCode", { id: await generateNewIdAsync(), action: 'create', data: { id: codeId, code, redirectUri: redirect_uri, oauthAppId: oauthApp.id, applicationId: context.getApplicationId(), userId: context.getCurrentUserId(), scope: scope === undefined ? [] : [scope], expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期 // PKCE 支持 codeChallenge: code_challenge, codeChallengeMethod: code_challenge_method || 'plain', } }, { dontCollect: true }); // 更新记录 await context.operate("oauthUserAuthorization", { id: await generateNewIdAsync(), action: 'update', data: { codeId: codeId, }, filter: { id: recordId, } }, {}); const params = new URLSearchParams(); params.set('code', code); if (state) { params.set('state', state); } closeRootMode(); return { redirectUri: `${redirect_uri}?${params.toString()}`, }; } closeRootMode(); throw new Error('unknown action'); } const fetchOAuthUserInfo = async (code, providerConfig) => { // 1. 使用 code 换取 access_token const tokenResponse = await fetch(providerConfig.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code: code, client_id: providerConfig.clientId, client_secret: providerConfig.clientSecret, redirect_uri: providerConfig.redirectUri, }), }); if (!tokenResponse.ok) { const errorjson = await tokenResponse.json(); if (errorjson.error == "unauthorized_client") { throw new OakUserException(`授权校验已过期,请重新发起授权请求`); } else if (errorjson.error == "invalid_grant") { throw new OakUserException(`授权码无效或已过期,请重新发起授权请求`); } else if (errorjson.error) { throw new OakUserException(`获取访问令牌失败: ${errorjson.error_description || errorjson.error}`); } throw new OakUserException(`获取访问令牌失败: ${tokenResponse.statusText}`); } const tokenData = await tokenResponse.json(); const accessToken = tokenData.access_token; const refreshToken = tokenData.refresh_token; const accessTokenExp = tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : undefined; const refreshTokenExp = tokenData.refresh_expires_in ? Date.now() + tokenData.refresh_expires_in * 1000 : undefined; const tokenType = tokenData.token_type; assert(tokenType && tokenType.toLowerCase() === 'bearer', '不支持的令牌类型'); // 2. 使用 access_token 获取用户信息 const userInfoResponse = await fetch(providerConfig.userInfoEndpoint, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, }, }); if (!userInfoResponse.ok) { throw new OakUserException(`获取用户信息失败: ${userInfoResponse.statusText}`); } const userInfoData = await userInfoResponse.json(); // TODO: 用户信息中获取唯一标识,通过注入解决: utils/oauth/index.ts const { id: providerUserId } = await processUserInfo(providerConfig.type, userInfoData); if (!providerUserId) { throw new OakUserException('用户信息中缺少唯一标识符'); } return { oauthUserInfo: { providerUserId, rawData: userInfoData, }, accessToken, refreshToken, accessTokenExp, refreshTokenExp, }; };