450 lines
16 KiB
JavaScript
450 lines
16 KiB
JavaScript
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, forUpdate: 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, forUpdate: 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,
|
||
};
|
||
};
|