feat: 在accessToken的endpoint和oauth的aspect中支持了PKCE相关支持

This commit is contained in:
Pan Qiancheng 2025-10-28 11:07:37 +08:00
parent 27db58a6f5
commit 989c3c66ad
3 changed files with 164 additions and 81 deletions

View File

@ -21,7 +21,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param from ID
@ -31,7 +31,7 @@ export type AspectDict<ED extends EntityDict> = {
params: { from: string; to: string },
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
*/
@ -39,7 +39,7 @@ export type AspectDict<ED extends EntityDict> = {
params: {},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param code code
@ -50,7 +50,7 @@ export type AspectDict<ED extends EntityDict> = {
params: { code: string; env: WechatMpEnv },
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param mobile
@ -65,7 +65,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param email
@ -80,7 +80,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param mobile
@ -98,7 +98,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param password SHA1
@ -111,7 +111,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
* //
* @param account
@ -127,7 +127,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param email
@ -145,7 +145,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param code code
@ -165,7 +165,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* 使 token
* @param tokenValue token
@ -174,7 +174,7 @@ export type AspectDict<ED extends EntityDict> = {
params: { tokenValue: string },
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param code code
@ -191,7 +191,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* APP
* @param code code
@ -208,7 +208,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param nickname
@ -233,7 +233,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
* shadow
* @param id ID
@ -247,7 +247,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* token
* @param tokenValue token
@ -263,7 +263,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param mobile
@ -279,7 +279,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param email
@ -295,7 +295,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param version
@ -315,7 +315,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* JS-SDK JS
* @param url URL
@ -334,7 +334,7 @@ export type AspectDict<ED extends EntityDict> = {
timestamp: number;
appId: string;
}>;
/**
*
* @param entity platform system
@ -349,7 +349,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param entity platform/system/application
@ -364,7 +364,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param entity application
@ -379,7 +379,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param userId ID
@ -390,7 +390,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param wechatQrCodeId ID
@ -400,7 +400,7 @@ export type AspectDict<ED extends EntityDict> = {
wechatQrCodeId: string,
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param type login-bind-
@ -414,7 +414,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param wechatUserId ID
@ -429,7 +429,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
* ID Web
* @param wechatLoginId ID
@ -443,7 +443,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* URL
* @param url URL
@ -454,7 +454,7 @@ export type AspectDict<ED extends EntityDict> = {
publishDate: number | undefined;
imageList: string[];
}>;
/**
*
* @param userId ID
@ -464,7 +464,7 @@ export type AspectDict<ED extends EntityDict> = {
params: { userId: string },
context: BackendRuntimeContext<ED>
) => Promise<string[]>;
/**
*
* @param userId ID
@ -484,7 +484,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<{ result: string; times?: number }>;
/**
*
* @param data
@ -502,7 +502,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param params ID
@ -512,7 +512,7 @@ export type AspectDict<ED extends EntityDict> = {
params: any,
context: BackendRuntimeContext<ED>
) => Promise<{ mediaId: string }>;
/**
* 使
* @param applicationId ID
@ -524,7 +524,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -536,7 +536,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -551,7 +551,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -566,7 +566,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -579,7 +579,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -590,7 +590,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -608,7 +608,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -622,7 +622,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -640,7 +640,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -656,7 +656,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -669,7 +669,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -683,7 +683,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -695,7 +695,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -710,7 +710,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -725,7 +725,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -737,7 +737,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @returns
@ -746,7 +746,7 @@ export type AspectDict<ED extends EntityDict> = {
params: {},
content: BackendRuntimeContext<ED>
) => Promise<string[]>;
/**
*
* @param applicationId ID
@ -759,7 +759,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -770,7 +770,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -784,7 +784,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -799,7 +799,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -814,7 +814,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -828,7 +828,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -842,7 +842,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -857,7 +857,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -870,7 +870,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
@ -885,7 +885,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param systemId ID
@ -898,7 +898,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param applicationId ID
@ -910,7 +910,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<EntityDict['applicationPassport']['Schema'][]>;
/**
* ID
* @param passportIds ID
@ -921,7 +921,7 @@ export type AspectDict<ED extends EntityDict> = {
},
content: BackendRuntimeContext<ED>
) => Promise<void>;
/**
* OAuth 2.0
* @param code OAuth
@ -937,7 +937,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* OAuth /
* @param providerId OAuth ID
@ -953,7 +953,7 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* OAuth 2.0
* @param response_type "code"
@ -972,12 +972,16 @@ export type AspectDict<ED extends EntityDict> = {
scope: string;
state: string;
action: "grant" | "deny";
// PKCE 支持
code_challenge?: string;
code_challenge_method?: 'plain' | 'S256';
},
context: BackendRuntimeContext<ED>
) => Promise<{
redirectUri: string;
}>
/**
* OAuth
* @param client_id ID

View File

@ -41,7 +41,7 @@ export async function loginByOauth<ED extends EntityDict>(params: {
filter: {
state: stateCode,
},
}, {})
}, { dontCollect: true })
assert(state, '无效的 state 参数');
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
@ -60,7 +60,7 @@ export async function loginByOauth<ED extends EntityDict>(params: {
filter: {
id: state.id,
}
}, {});
}, { dontCollect: true });
// 使用 code 换取 access_token 并获取用户信息
const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(
@ -87,7 +87,7 @@ export async function loginByOauth<ED extends EntityDict>(params: {
providerUserId: oauthUserInfo.providerUserId,
providerConfigId: state.providerId!,
}
}, {})
}, { dontCollect: true })
// 已登录的情况
if (islogginedIn) {
@ -130,7 +130,7 @@ export async function loginByOauth<ED extends EntityDict>(params: {
applicationId,
stateId: state.id,
}
}, {});
}, { dontCollect: true });
// 返回当前 token
const tokenValue = context.getTokenValue()!;
@ -171,7 +171,7 @@ export async function loginByOauth<ED extends EntityDict>(params: {
filter: {
id: existingOAuthUser.id,
}
}, {});
}, { dontCollect: true });
await loadTokenInfo(tokenValue, context);
closeRootMode();
@ -219,7 +219,7 @@ export async function loginByOauth<ED extends EntityDict>(params: {
filter: {
id: newUserId,
}
}, {});
}, { dontCollect: true });
await loadTokenInfo(tokenValue, context);
closeRootMode();
@ -248,7 +248,7 @@ export async function getOAuthClientInfo<ED extends EntityDict>(params: {
systemId: systemId,
ableState: "enabled",
}
}, {})
}, { dontCollect: true })
// 如果还有正在生效的授权,说明已经授权过了
const [hasAuth] = await context.select("oauthUserAuthorization", {
@ -279,7 +279,7 @@ export async function getOAuthClientInfo<ED extends EntityDict>(params: {
userId: currentUserId,
}
}
}, {})
}, { dontCollect: true })
if (hasAuth) {
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
@ -318,7 +318,7 @@ export async function createOAuthState<ED extends EntityDict>(params: {
type,
state
}
}, {})
}, { dontCollect: true })
closeRootMode();
return state;
@ -330,8 +330,12 @@ export async function authorize<ED extends EntityDict>(params: {
scope?: string;
state?: string;
action: 'grant' | 'deny';
// PKCE 支持
code_challenge?: string;
code_challenge_method?: 'plain' | 'S256';
}, context: BRC<ED>) {
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 类型');
@ -350,7 +354,7 @@ export async function authorize<ED extends EntityDict>(params: {
id: client_id,
systemId: systemId,
}
}, {})
}, { dontCollect: true })
if (!oauthApp) {
throw new OakUserException('未经授权的客户端应用');
@ -368,7 +372,7 @@ export async function authorize<ED extends EntityDict>(params: {
usageState: action === 'grant' ? 'unused' : 'denied',
authorizedAt: Date.now(),
}
}, {})
}, { dontCollect: true })
if (action === 'deny') {
const params = new URLSearchParams();
@ -407,8 +411,12 @@ export async function authorize<ED extends EntityDict>(params: {
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", {

View File

@ -8,6 +8,27 @@ import { applicationProjection, extraFileProjection } from "../types/Projection"
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: string,
challenge: string,
method: 'plain' | 'S256'
): Promise<boolean> {
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: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
name: "获取OAuth Token",
@ -16,7 +37,21 @@ const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const context = await contextBuilder()
const { client_id, client_secret, grant_type, code, redirect_uri } = body as { client_id: string, client_secret: string, grant_type: string, code?: string, redirect_uri?: string };
const {
client_id,
client_secret,
grant_type,
code,
redirect_uri,
code_verifier // PKCE支持
} = body as {
client_id: string,
client_secret: string,
grant_type: string,
code?: string,
redirect_uri?: string
code_verifier?: string
};
const [app] = await context.select("oauthApplication", {
data: {
id: 1,
@ -61,6 +96,8 @@ const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>
userId: 1,
expiresAt: 1,
usedAt: 1,
codeChallenge: 1,
codeChallengeMethod: 1,
},
filter: {
code,
@ -76,6 +113,40 @@ const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>
};
}
// 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 as string,
authCodeRecord.codeChallengeMethod as 'plain' | 'S256'
);
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();