build
This commit is contained in:
parent
44c969635f
commit
a2dfa1d1f3
|
|
@ -710,6 +710,8 @@ export type AspectDict<ED extends EntityDict> = {
|
||||||
scope: string;
|
scope: string;
|
||||||
state: string;
|
state: string;
|
||||||
action: "grant" | "deny";
|
action: "grant" | "deny";
|
||||||
|
code_challenge?: string;
|
||||||
|
code_challenge_method?: 'plain' | 'S256';
|
||||||
}, context: BackendRuntimeContext<ED>) => Promise<{
|
}, context: BackendRuntimeContext<ED>) => Promise<{
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ export declare function authorize<ED extends EntityDict>(params: {
|
||||||
scope?: string;
|
scope?: string;
|
||||||
state?: string;
|
state?: string;
|
||||||
action: 'grant' | 'deny';
|
action: 'grant' | 'deny';
|
||||||
|
code_challenge?: string;
|
||||||
|
code_challenge_method?: 'plain' | 'S256';
|
||||||
}, context: BRC<ED>): Promise<{
|
}, context: BRC<ED>): Promise<{
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export async function loginByOauth(params, context) {
|
||||||
filter: {
|
filter: {
|
||||||
state: stateCode,
|
state: stateCode,
|
||||||
},
|
},
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
assert(state, '无效的 state 参数');
|
assert(state, '无效的 state 参数');
|
||||||
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
|
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
|
||||||
// 如果已经使用
|
// 如果已经使用
|
||||||
|
|
@ -48,7 +48,7 @@ export async function loginByOauth(params, context) {
|
||||||
filter: {
|
filter: {
|
||||||
id: state.id,
|
id: state.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 使用 code 换取 access_token 并获取用户信息
|
// 使用 code 换取 access_token 并获取用户信息
|
||||||
const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(code, state.provider);
|
const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(code, state.provider);
|
||||||
const [existingOAuthUser] = await context.select("oauthUser", {
|
const [existingOAuthUser] = await context.select("oauthUser", {
|
||||||
|
|
@ -70,7 +70,7 @@ export async function loginByOauth(params, context) {
|
||||||
providerUserId: oauthUserInfo.providerUserId,
|
providerUserId: oauthUserInfo.providerUserId,
|
||||||
providerConfigId: state.providerId,
|
providerConfigId: state.providerId,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 已登录的情况
|
// 已登录的情况
|
||||||
if (islogginedIn) {
|
if (islogginedIn) {
|
||||||
// 检查当前用户是否已绑定此提供商
|
// 检查当前用户是否已绑定此提供商
|
||||||
|
|
@ -107,7 +107,7 @@ export async function loginByOauth(params, context) {
|
||||||
applicationId,
|
applicationId,
|
||||||
stateId: state.id,
|
stateId: state.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 返回当前 token
|
// 返回当前 token
|
||||||
const tokenValue = context.getTokenValue();
|
const tokenValue = context.getTokenValue();
|
||||||
await loadTokenInfo(tokenValue, context);
|
await loadTokenInfo(tokenValue, context);
|
||||||
|
|
@ -138,7 +138,7 @@ export async function loginByOauth(params, context) {
|
||||||
filter: {
|
filter: {
|
||||||
id: existingOAuthUser.id,
|
id: existingOAuthUser.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
await loadTokenInfo(tokenValue, context);
|
await loadTokenInfo(tokenValue, context);
|
||||||
closeRootMode();
|
closeRootMode();
|
||||||
return tokenValue;
|
return tokenValue;
|
||||||
|
|
@ -174,7 +174,7 @@ export async function loginByOauth(params, context) {
|
||||||
filter: {
|
filter: {
|
||||||
id: newUserId,
|
id: newUserId,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
await loadTokenInfo(tokenValue, context);
|
await loadTokenInfo(tokenValue, context);
|
||||||
closeRootMode();
|
closeRootMode();
|
||||||
return tokenValue;
|
return tokenValue;
|
||||||
|
|
@ -199,7 +199,7 @@ export async function getOAuthClientInfo(params, context) {
|
||||||
systemId: systemId,
|
systemId: systemId,
|
||||||
ableState: "enabled",
|
ableState: "enabled",
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 如果还有正在生效的授权,说明已经授权过了
|
// 如果还有正在生效的授权,说明已经授权过了
|
||||||
const [hasAuth] = await context.select("oauthUserAuthorization", {
|
const [hasAuth] = await context.select("oauthUserAuthorization", {
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -229,7 +229,7 @@ export async function getOAuthClientInfo(params, context) {
|
||||||
userId: currentUserId,
|
userId: currentUserId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
if (hasAuth) {
|
if (hasAuth) {
|
||||||
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
|
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
|
||||||
}
|
}
|
||||||
|
|
@ -259,12 +259,12 @@ export async function createOAuthState(params, context) {
|
||||||
type,
|
type,
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
closeRootMode();
|
closeRootMode();
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
export async function authorize(params, context) {
|
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') {
|
if (response_type !== 'code') {
|
||||||
throw new OakUserException('不支持的 response_type 类型');
|
throw new OakUserException('不支持的 response_type 类型');
|
||||||
}
|
}
|
||||||
|
|
@ -280,7 +280,7 @@ export async function authorize(params, context) {
|
||||||
id: client_id,
|
id: client_id,
|
||||||
systemId: systemId,
|
systemId: systemId,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
if (!oauthApp) {
|
if (!oauthApp) {
|
||||||
throw new OakUserException('未经授权的客户端应用');
|
throw new OakUserException('未经授权的客户端应用');
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +296,7 @@ export async function authorize(params, context) {
|
||||||
usageState: action === 'grant' ? 'unused' : 'denied',
|
usageState: action === 'grant' ? 'unused' : 'denied',
|
||||||
authorizedAt: Date.now(),
|
authorizedAt: Date.now(),
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
if (action === 'deny') {
|
if (action === 'deny') {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('error', 'access_denied');
|
params.set('error', 'access_denied');
|
||||||
|
|
@ -330,8 +330,11 @@ export async function authorize(params, context) {
|
||||||
userId: context.getCurrentUserId(),
|
userId: context.getCurrentUserId(),
|
||||||
scope: scope === undefined ? [] : [scope],
|
scope: scope === undefined ? [] : [scope],
|
||||||
expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
|
expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
|
||||||
|
// PKCE 支持
|
||||||
|
codeChallenge: code_challenge,
|
||||||
|
codeChallengeMethod: code_challenge_method || 'plain',
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 更新记录
|
// 更新记录
|
||||||
await context.operate("oauthUserAuthorization", {
|
await context.operate("oauthUserAuthorization", {
|
||||||
id: await generateNewIdAsync(),
|
id: await generateNewIdAsync(),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,22 @@ import { randomUUID } from "crypto";
|
||||||
import { composeFileUrl } from "../utils/cos/index.backend";
|
import { composeFileUrl } from "../utils/cos/index.backend";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import { checkOauthTokenAvaliable } from "../utils/oauth";
|
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 = {
|
const oauthTokenEndpoint = {
|
||||||
name: "获取OAuth Token",
|
name: "获取OAuth Token",
|
||||||
params: [],
|
params: [],
|
||||||
|
|
@ -10,7 +26,8 @@ const oauthTokenEndpoint = {
|
||||||
type: "free",
|
type: "free",
|
||||||
fn: async (contextBuilder, params, header, req, body) => {
|
fn: async (contextBuilder, params, header, req, body) => {
|
||||||
const context = await contextBuilder();
|
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", {
|
const [app] = await context.select("oauthApplication", {
|
||||||
data: {
|
data: {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|
@ -51,6 +68,8 @@ const oauthTokenEndpoint = {
|
||||||
userId: 1,
|
userId: 1,
|
||||||
expiresAt: 1,
|
expiresAt: 1,
|
||||||
usedAt: 1,
|
usedAt: 1,
|
||||||
|
codeChallenge: 1,
|
||||||
|
codeChallengeMethod: 1,
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
code,
|
code,
|
||||||
|
|
@ -64,6 +83,33 @@ const oauthTokenEndpoint = {
|
||||||
data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false }
|
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
|
// 验证redirect_uri
|
||||||
if (authCodeRecord.redirectUri !== redirect_uri) {
|
if (authCodeRecord.redirectUri !== redirect_uri) {
|
||||||
await context.commit();
|
await context.commit();
|
||||||
|
|
|
||||||
|
|
@ -710,6 +710,8 @@ export type AspectDict<ED extends EntityDict> = {
|
||||||
scope: string;
|
scope: string;
|
||||||
state: string;
|
state: string;
|
||||||
action: "grant" | "deny";
|
action: "grant" | "deny";
|
||||||
|
code_challenge?: string;
|
||||||
|
code_challenge_method?: 'plain' | 'S256';
|
||||||
}, context: BackendRuntimeContext<ED>) => Promise<{
|
}, context: BackendRuntimeContext<ED>) => Promise<{
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ export declare function authorize<ED extends EntityDict>(params: {
|
||||||
scope?: string;
|
scope?: string;
|
||||||
state?: string;
|
state?: string;
|
||||||
action: 'grant' | 'deny';
|
action: 'grant' | 'deny';
|
||||||
|
code_challenge?: string;
|
||||||
|
code_challenge_method?: 'plain' | 'S256';
|
||||||
}, context: BRC<ED>): Promise<{
|
}, context: BRC<ED>): Promise<{
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ async function loginByOauth(params, context) {
|
||||||
filter: {
|
filter: {
|
||||||
state: stateCode,
|
state: stateCode,
|
||||||
},
|
},
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
(0, assert_1.default)(state, '无效的 state 参数');
|
(0, assert_1.default)(state, '无效的 state 参数');
|
||||||
(0, assert_1.default)(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
|
(0, assert_1.default)(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
|
||||||
// 如果已经使用
|
// 如果已经使用
|
||||||
|
|
@ -52,7 +52,7 @@ async function loginByOauth(params, context) {
|
||||||
filter: {
|
filter: {
|
||||||
id: state.id,
|
id: state.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 使用 code 换取 access_token 并获取用户信息
|
// 使用 code 换取 access_token 并获取用户信息
|
||||||
const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(code, state.provider);
|
const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(code, state.provider);
|
||||||
const [existingOAuthUser] = await context.select("oauthUser", {
|
const [existingOAuthUser] = await context.select("oauthUser", {
|
||||||
|
|
@ -74,7 +74,7 @@ async function loginByOauth(params, context) {
|
||||||
providerUserId: oauthUserInfo.providerUserId,
|
providerUserId: oauthUserInfo.providerUserId,
|
||||||
providerConfigId: state.providerId,
|
providerConfigId: state.providerId,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 已登录的情况
|
// 已登录的情况
|
||||||
if (islogginedIn) {
|
if (islogginedIn) {
|
||||||
// 检查当前用户是否已绑定此提供商
|
// 检查当前用户是否已绑定此提供商
|
||||||
|
|
@ -111,7 +111,7 @@ async function loginByOauth(params, context) {
|
||||||
applicationId,
|
applicationId,
|
||||||
stateId: state.id,
|
stateId: state.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 返回当前 token
|
// 返回当前 token
|
||||||
const tokenValue = context.getTokenValue();
|
const tokenValue = context.getTokenValue();
|
||||||
await (0, token_1.loadTokenInfo)(tokenValue, context);
|
await (0, token_1.loadTokenInfo)(tokenValue, context);
|
||||||
|
|
@ -142,7 +142,7 @@ async function loginByOauth(params, context) {
|
||||||
filter: {
|
filter: {
|
||||||
id: existingOAuthUser.id,
|
id: existingOAuthUser.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
await (0, token_1.loadTokenInfo)(tokenValue, context);
|
await (0, token_1.loadTokenInfo)(tokenValue, context);
|
||||||
closeRootMode();
|
closeRootMode();
|
||||||
return tokenValue;
|
return tokenValue;
|
||||||
|
|
@ -178,7 +178,7 @@ async function loginByOauth(params, context) {
|
||||||
filter: {
|
filter: {
|
||||||
id: newUserId,
|
id: newUserId,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
await (0, token_1.loadTokenInfo)(tokenValue, context);
|
await (0, token_1.loadTokenInfo)(tokenValue, context);
|
||||||
closeRootMode();
|
closeRootMode();
|
||||||
return tokenValue;
|
return tokenValue;
|
||||||
|
|
@ -204,7 +204,7 @@ async function getOAuthClientInfo(params, context) {
|
||||||
systemId: systemId,
|
systemId: systemId,
|
||||||
ableState: "enabled",
|
ableState: "enabled",
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 如果还有正在生效的授权,说明已经授权过了
|
// 如果还有正在生效的授权,说明已经授权过了
|
||||||
const [hasAuth] = await context.select("oauthUserAuthorization", {
|
const [hasAuth] = await context.select("oauthUserAuthorization", {
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -234,7 +234,7 @@ async function getOAuthClientInfo(params, context) {
|
||||||
userId: currentUserId,
|
userId: currentUserId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
if (hasAuth) {
|
if (hasAuth) {
|
||||||
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
|
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
|
||||||
}
|
}
|
||||||
|
|
@ -265,13 +265,13 @@ async function createOAuthState(params, context) {
|
||||||
type,
|
type,
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
closeRootMode();
|
closeRootMode();
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
exports.createOAuthState = createOAuthState;
|
exports.createOAuthState = createOAuthState;
|
||||||
async function authorize(params, context) {
|
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') {
|
if (response_type !== 'code') {
|
||||||
throw new types_1.OakUserException('不支持的 response_type 类型');
|
throw new types_1.OakUserException('不支持的 response_type 类型');
|
||||||
}
|
}
|
||||||
|
|
@ -287,7 +287,7 @@ async function authorize(params, context) {
|
||||||
id: client_id,
|
id: client_id,
|
||||||
systemId: systemId,
|
systemId: systemId,
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
if (!oauthApp) {
|
if (!oauthApp) {
|
||||||
throw new types_1.OakUserException('未经授权的客户端应用');
|
throw new types_1.OakUserException('未经授权的客户端应用');
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +303,7 @@ async function authorize(params, context) {
|
||||||
usageState: action === 'grant' ? 'unused' : 'denied',
|
usageState: action === 'grant' ? 'unused' : 'denied',
|
||||||
authorizedAt: Date.now(),
|
authorizedAt: Date.now(),
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
if (action === 'deny') {
|
if (action === 'deny') {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('error', 'access_denied');
|
params.set('error', 'access_denied');
|
||||||
|
|
@ -337,8 +337,11 @@ async function authorize(params, context) {
|
||||||
userId: context.getCurrentUserId(),
|
userId: context.getCurrentUserId(),
|
||||||
scope: scope === undefined ? [] : [scope],
|
scope: scope === undefined ? [] : [scope],
|
||||||
expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
|
expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
|
||||||
|
// PKCE 支持
|
||||||
|
codeChallenge: code_challenge,
|
||||||
|
codeChallengeMethod: code_challenge_method || 'plain',
|
||||||
}
|
}
|
||||||
}, {});
|
}, { dontCollect: true });
|
||||||
// 更新记录
|
// 更新记录
|
||||||
await context.operate("oauthUserAuthorization", {
|
await context.operate("oauthUserAuthorization", {
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,22 @@ const crypto_1 = require("crypto");
|
||||||
const index_backend_1 = require("../utils/cos/index.backend");
|
const index_backend_1 = require("../utils/cos/index.backend");
|
||||||
const assert_1 = tslib_1.__importDefault(require("assert"));
|
const assert_1 = tslib_1.__importDefault(require("assert"));
|
||||||
const oauth_1 = require("../utils/oauth");
|
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 = {
|
const oauthTokenEndpoint = {
|
||||||
name: "获取OAuth Token",
|
name: "获取OAuth Token",
|
||||||
params: [],
|
params: [],
|
||||||
|
|
@ -13,7 +29,8 @@ const oauthTokenEndpoint = {
|
||||||
type: "free",
|
type: "free",
|
||||||
fn: async (contextBuilder, params, header, req, body) => {
|
fn: async (contextBuilder, params, header, req, body) => {
|
||||||
const context = await contextBuilder();
|
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", {
|
const [app] = await context.select("oauthApplication", {
|
||||||
data: {
|
data: {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|
@ -54,6 +71,8 @@ const oauthTokenEndpoint = {
|
||||||
userId: 1,
|
userId: 1,
|
||||||
expiresAt: 1,
|
expiresAt: 1,
|
||||||
usedAt: 1,
|
usedAt: 1,
|
||||||
|
codeChallenge: 1,
|
||||||
|
codeChallengeMethod: 1,
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
code,
|
code,
|
||||||
|
|
@ -67,6 +86,33 @@ const oauthTokenEndpoint = {
|
||||||
data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false }
|
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
|
// 验证redirect_uri
|
||||||
if (authCodeRecord.redirectUri !== redirect_uri) {
|
if (authCodeRecord.redirectUri !== redirect_uri) {
|
||||||
await context.commit();
|
await context.commit();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue