This commit is contained in:
Pan Qiancheng 2025-10-28 11:21:30 +08:00
parent 44c969635f
commit a2dfa1d1f3
8 changed files with 134 additions and 28 deletions

View File

@ -710,6 +710,8 @@ export type AspectDict<ED extends EntityDict> = {
scope: string;
state: string;
action: "grant" | "deny";
code_challenge?: string;
code_challenge_method?: 'plain' | 'S256';
}, context: BackendRuntimeContext<ED>) => Promise<{
redirectUri: string;
}>;

View File

@ -25,6 +25,8 @@ export declare function authorize<ED extends EntityDict>(params: {
scope?: string;
state?: string;
action: 'grant' | 'deny';
code_challenge?: string;
code_challenge_method?: 'plain' | 'S256';
}, context: BRC<ED>): Promise<{
redirectUri: string;
}>;

View File

@ -31,7 +31,7 @@ export async function loginByOauth(params, context) {
filter: {
state: stateCode,
},
}, {});
}, { dontCollect: true });
assert(state, '无效的 state 参数');
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
// 如果已经使用
@ -48,7 +48,7 @@ export async function loginByOauth(params, context) {
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", {
@ -70,7 +70,7 @@ export async function loginByOauth(params, context) {
providerUserId: oauthUserInfo.providerUserId,
providerConfigId: state.providerId,
}
}, {});
}, { dontCollect: true });
// 已登录的情况
if (islogginedIn) {
// 检查当前用户是否已绑定此提供商
@ -107,7 +107,7 @@ export async function loginByOauth(params, context) {
applicationId,
stateId: state.id,
}
}, {});
}, { dontCollect: true });
// 返回当前 token
const tokenValue = context.getTokenValue();
await loadTokenInfo(tokenValue, context);
@ -138,7 +138,7 @@ export async function loginByOauth(params, context) {
filter: {
id: existingOAuthUser.id,
}
}, {});
}, { dontCollect: true });
await loadTokenInfo(tokenValue, context);
closeRootMode();
return tokenValue;
@ -174,7 +174,7 @@ export async function loginByOauth(params, context) {
filter: {
id: newUserId,
}
}, {});
}, { dontCollect: true });
await loadTokenInfo(tokenValue, context);
closeRootMode();
return tokenValue;
@ -199,7 +199,7 @@ export async function getOAuthClientInfo(params, context) {
systemId: systemId,
ableState: "enabled",
}
}, {});
}, { dontCollect: true });
// 如果还有正在生效的授权,说明已经授权过了
const [hasAuth] = await context.select("oauthUserAuthorization", {
data: {
@ -229,7 +229,7 @@ export async function getOAuthClientInfo(params, context) {
userId: currentUserId,
}
}
}, {});
}, { dontCollect: true });
if (hasAuth) {
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
}
@ -259,12 +259,12 @@ export async function createOAuthState(params, context) {
type,
state
}
}, {});
}, { dontCollect: true });
closeRootMode();
return state;
}
export async function authorize(params, context) {
const { response_type, client_id, redirect_uri, scope, state, action } = params;
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 类型');
}
@ -280,7 +280,7 @@ export async function authorize(params, context) {
id: client_id,
systemId: systemId,
}
}, {});
}, { dontCollect: true });
if (!oauthApp) {
throw new OakUserException('未经授权的客户端应用');
}
@ -296,7 +296,7 @@ export async function authorize(params, context) {
usageState: action === 'grant' ? 'unused' : 'denied',
authorizedAt: Date.now(),
}
}, {});
}, { dontCollect: true });
if (action === 'deny') {
const params = new URLSearchParams();
params.set('error', 'access_denied');
@ -330,8 +330,11 @@ export async function authorize(params, context) {
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(),

View File

@ -3,6 +3,22 @@ import { randomUUID } from "crypto";
import { composeFileUrl } from "../utils/cos/index.backend";
import assert from "assert";
import { checkOauthTokenAvaliable } from "../utils/oauth";
import { createHash } from "crypto";
// PKCE 验证函数
async function verifyPKCE(verifier, challenge, method) {
if (method === 'plain') {
return verifier === challenge;
}
else if (method === 'S256') {
const hash = createHash('sha256').update(verifier).digest();
const computed = hash.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return computed === challenge;
}
return false;
}
const oauthTokenEndpoint = {
name: "获取OAuth Token",
params: [],
@ -10,7 +26,8 @@ const oauthTokenEndpoint = {
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const context = await contextBuilder();
const { client_id, client_secret, grant_type, code, redirect_uri } = body;
const { client_id, client_secret, grant_type, code, redirect_uri, code_verifier // PKCE支持
} = body;
const [app] = await context.select("oauthApplication", {
data: {
id: 1,
@ -51,6 +68,8 @@ const oauthTokenEndpoint = {
userId: 1,
expiresAt: 1,
usedAt: 1,
codeChallenge: 1,
codeChallengeMethod: 1,
},
filter: {
code,
@ -64,6 +83,33 @@ const oauthTokenEndpoint = {
data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false }
};
}
// PKCE 验证
if (authCodeRecord.codeChallenge) {
if (!code_verifier) {
await context.commit();
return {
statusCode: 400,
data: {
error: "invalid_request",
error_description: "code_verifier is required",
success: false
}
};
}
// 验证 code_verifier
const isValid = await verifyPKCE(code_verifier, authCodeRecord.codeChallenge, authCodeRecord.codeChallengeMethod);
if (!isValid) {
await context.commit();
return {
statusCode: 400,
data: {
error: "invalid_grant",
error_description: "Invalid code_verifier",
success: false
}
};
}
}
// 验证redirect_uri
if (authCodeRecord.redirectUri !== redirect_uri) {
await context.commit();

View File

@ -710,6 +710,8 @@ export type AspectDict<ED extends EntityDict> = {
scope: string;
state: string;
action: "grant" | "deny";
code_challenge?: string;
code_challenge_method?: 'plain' | 'S256';
}, context: BackendRuntimeContext<ED>) => Promise<{
redirectUri: string;
}>;

View File

@ -25,6 +25,8 @@ export declare function authorize<ED extends EntityDict>(params: {
scope?: string;
state?: string;
action: 'grant' | 'deny';
code_challenge?: string;
code_challenge_method?: 'plain' | 'S256';
}, context: BRC<ED>): Promise<{
redirectUri: string;
}>;

View File

@ -35,7 +35,7 @@ async function loginByOauth(params, context) {
filter: {
state: stateCode,
},
}, {});
}, { dontCollect: true });
(0, assert_1.default)(state, '无效的 state 参数');
(0, assert_1.default)(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
// 如果已经使用
@ -52,7 +52,7 @@ async function loginByOauth(params, context) {
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", {
@ -74,7 +74,7 @@ async function loginByOauth(params, context) {
providerUserId: oauthUserInfo.providerUserId,
providerConfigId: state.providerId,
}
}, {});
}, { dontCollect: true });
// 已登录的情况
if (islogginedIn) {
// 检查当前用户是否已绑定此提供商
@ -111,7 +111,7 @@ async function loginByOauth(params, context) {
applicationId,
stateId: state.id,
}
}, {});
}, { dontCollect: true });
// 返回当前 token
const tokenValue = context.getTokenValue();
await (0, token_1.loadTokenInfo)(tokenValue, context);
@ -142,7 +142,7 @@ async function loginByOauth(params, context) {
filter: {
id: existingOAuthUser.id,
}
}, {});
}, { dontCollect: true });
await (0, token_1.loadTokenInfo)(tokenValue, context);
closeRootMode();
return tokenValue;
@ -178,7 +178,7 @@ async function loginByOauth(params, context) {
filter: {
id: newUserId,
}
}, {});
}, { dontCollect: true });
await (0, token_1.loadTokenInfo)(tokenValue, context);
closeRootMode();
return tokenValue;
@ -204,7 +204,7 @@ async function getOAuthClientInfo(params, context) {
systemId: systemId,
ableState: "enabled",
}
}, {});
}, { dontCollect: true });
// 如果还有正在生效的授权,说明已经授权过了
const [hasAuth] = await context.select("oauthUserAuthorization", {
data: {
@ -234,7 +234,7 @@ async function getOAuthClientInfo(params, context) {
userId: currentUserId,
}
}
}, {});
}, { dontCollect: true });
if (hasAuth) {
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
}
@ -265,13 +265,13 @@ async function createOAuthState(params, context) {
type,
state
}
}, {});
}, { dontCollect: true });
closeRootMode();
return state;
}
exports.createOAuthState = createOAuthState;
async function authorize(params, context) {
const { response_type, client_id, redirect_uri, scope, state, action } = params;
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 类型');
}
@ -287,7 +287,7 @@ async function authorize(params, context) {
id: client_id,
systemId: systemId,
}
}, {});
}, { dontCollect: true });
if (!oauthApp) {
throw new types_1.OakUserException('未经授权的客户端应用');
}
@ -303,7 +303,7 @@ async function authorize(params, context) {
usageState: action === 'grant' ? 'unused' : 'denied',
authorizedAt: Date.now(),
}
}, {});
}, { dontCollect: true });
if (action === 'deny') {
const params = new URLSearchParams();
params.set('error', 'access_denied');
@ -337,8 +337,11 @@ async function authorize(params, context) {
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)(),

View File

@ -6,6 +6,22 @@ const crypto_1 = require("crypto");
const index_backend_1 = require("../utils/cos/index.backend");
const assert_1 = tslib_1.__importDefault(require("assert"));
const oauth_1 = require("../utils/oauth");
const crypto_2 = require("crypto");
// PKCE 验证函数
async function verifyPKCE(verifier, challenge, method) {
if (method === 'plain') {
return verifier === challenge;
}
else if (method === 'S256') {
const hash = (0, crypto_2.createHash)('sha256').update(verifier).digest();
const computed = hash.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return computed === challenge;
}
return false;
}
const oauthTokenEndpoint = {
name: "获取OAuth Token",
params: [],
@ -13,7 +29,8 @@ const oauthTokenEndpoint = {
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const context = await contextBuilder();
const { client_id, client_secret, grant_type, code, redirect_uri } = body;
const { client_id, client_secret, grant_type, code, redirect_uri, code_verifier // PKCE支持
} = body;
const [app] = await context.select("oauthApplication", {
data: {
id: 1,
@ -54,6 +71,8 @@ const oauthTokenEndpoint = {
userId: 1,
expiresAt: 1,
usedAt: 1,
codeChallenge: 1,
codeChallengeMethod: 1,
},
filter: {
code,
@ -67,6 +86,33 @@ const oauthTokenEndpoint = {
data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false }
};
}
// PKCE 验证
if (authCodeRecord.codeChallenge) {
if (!code_verifier) {
await context.commit();
return {
statusCode: 400,
data: {
error: "invalid_request",
error_description: "code_verifier is required",
success: false
}
};
}
// 验证 code_verifier
const isValid = await verifyPKCE(code_verifier, authCodeRecord.codeChallenge, authCodeRecord.codeChallengeMethod);
if (!isValid) {
await context.commit();
return {
statusCode: 400,
data: {
error: "invalid_grant",
error_description: "Invalid code_verifier",
success: false
}
};
}
}
// 验证redirect_uri
if (authCodeRecord.redirectUri !== redirect_uri) {
await context.commit();