oak-general-business/lib/aspects/oauth.js

457 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loginByOauth = loginByOauth;
exports.getOAuthClientInfo = getOAuthClientInfo;
exports.createOAuthState = createOAuthState;
exports.authorize = authorize;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const types_1 = require("oak-domain/lib/types");
const uuid_1 = require("oak-domain/lib/utils/uuid");
const token_1 = require("./token");
const crypto_1 = require("crypto");
const oauth_1 = require("../utils/oauth");
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;
(0, assert_1.default)(applicationId, '无法获取当前应用ID');
(0, assert_1.default)(code, 'code 参数缺失');
(0, assert_1.default)(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 types_1.OakUserException('error::user.loginWayDisabled');
}
(0, assert_1.default)(state, '无效的 state 参数');
(0, assert_1.default)(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
// 如果已经使用
if (state.usedAt) {
throw new types_1.OakUserException('该授权请求已被使用,请重新发起授权请求');
}
// 更新为使用过
await context.operate("oauthState", {
id: await (0, uuid_1.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 types_1.OakUserException('当前用户已绑定该 OAuth 平台账号');
}
if (existingOAuthUser) {
throw new types_1.OakUserException('该 OAuth 账号已被其他用户绑定');
}
console.log("绑定 OAuth 账号到当前用户:", currentUserId, oauthUserInfo.providerUserId);
// 创建绑定关系
await context.operate("oauthUser", {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.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 (0, token_1.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 (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', existingOAuthUser.id, // 使用已存在的 oauthUser ID
undefined, targetUser // 关联的用户
);
// 更新登录信息
await context.operate("oauthUser", {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'update',
data: {
rawUserInfo: oauthUserInfo.rawData,
accessToken,
accessExpiresAt: accessTokenExp,
refreshToken,
refreshExpiresAt: refreshTokenExp,
applicationId,
stateId: state.id,
},
filter: {
id: existingOAuthUser.id,
}
}, { dontCollect: true });
await (0, token_1.loadTokenInfo)(tokenValue, context);
closeRootMode();
return tokenValue;
}
// 未登录OAuth账号不存在创建新用户
else {
if (!state.provider.autoRegister) {
throw new types_1.OakUserException('您还没有账号,请先注册一个账号');
}
console.log("使用未绑定的 OAuth 账号登录:", oauthUserInfo.providerUserId);
const newUserId = await (0, uuid_1.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 (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', undefined, oauthUserCreateData, // 创建新的 oauthUser
undefined // 不传 user自动创建新用户
);
await context.operate("oauthUser", {
id: await (0, uuid_1.generateNewIdAsync)(),
action: 'loadUserInfo',
data: {},
filter: {
id: newUserId,
}
}, { dontCollect: true });
await (0, token_1.loadTokenInfo)(tokenValue, context);
closeRootMode();
return tokenValue;
}
}
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 types_1.OakUserException('未经授权的客户端应用');
}
closeRootMode();
return {
data: oauthApp,
alreadyAuth: !!hasAuth,
};
}
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 (0, uuid_1.generateNewIdAsync)(),
action: 'create',
data: {
id: await (0, uuid_1.generateNewIdAsync)(),
providerId,
userId,
type,
state
}
}, { dontCollect: true });
closeRootMode();
return state;
}
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 types_1.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 types_1.OakUserException('未经授权的客户端应用');
}
// 创建授权记录
const recordId = await (0, uuid_1.generateNewIdAsync)();
await context.operate("oauthUserAuthorization", {
id: await (0, uuid_1.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 types_1.OakUserException('重定向 URI 不合法');
}
const code = (0, crypto_1.randomUUID)();
const codeId = await (0, uuid_1.generateNewIdAsync)();
// 存储授权码
await context.operate("oauthAuthorizationCode", {
id: await (0, uuid_1.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 (0, uuid_1.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 types_1.OakUserException(`授权校验已过期,请重新发起授权请求`);
}
else if (errorjson.error == "invalid_grant") {
throw new types_1.OakUserException(`授权码无效或已过期,请重新发起授权请求`);
}
else if (errorjson.error) {
throw new types_1.OakUserException(`获取访问令牌失败: ${errorjson.error_description || errorjson.error}`);
}
throw new types_1.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;
(0, assert_1.default)(tokenType && tokenType.toLowerCase() === 'bearer', '不支持的令牌类型');
// 2. 使用 access_token 获取用户信息
const userInfoResponse = await fetch(providerConfig.userInfoEndpoint, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!userInfoResponse.ok) {
throw new types_1.OakUserException(`获取用户信息失败: ${userInfoResponse.statusText}`);
}
const userInfoData = await userInfoResponse.json();
// TODO: 用户信息中获取唯一标识,通过注入解决: utils/oauth/index.ts
const { id: providerUserId } = await (0, oauth_1.processUserInfo)(providerConfig.type, userInfoData);
if (!providerUserId) {
throw new types_1.OakUserException('用户信息中缺少唯一标识符');
}
return {
oauthUserInfo: {
providerUserId,
rawData: userInfoData,
},
accessToken,
refreshToken,
accessTokenExp,
refreshTokenExp,
};
};