feat: 如果是在某一应用下反复授权则直接允许,并修改实体添加了更多认真状态

This commit is contained in:
Pan Qiancheng 2025-10-23 22:25:38 +08:00
parent 9a05b713ab
commit 0dc7dd4d98
16 changed files with 524 additions and 22 deletions

View File

@ -720,6 +720,9 @@ export type AspectDict<ED extends EntityDict> = {
*/
getOAuthClientInfo: (params: {
client_id: string;
}, context: BackendRuntimeContext<ED>) => Promise<EntityDict['oauthApplication']['Schema'] | null>;
}, context: BackendRuntimeContext<ED>) => Promise<{
data: EntityDict['oauthApplication']['Schema'] | null;
alreadyAuth: boolean;
}>;
};
export default AspectDict;

View File

@ -8,7 +8,10 @@ export declare function loginByOauth<ED extends EntityDict>(params: {
}, context: BRC<ED>): Promise<string>;
export declare function getOAuthClientInfo<ED extends EntityDict>(params: {
client_id: string;
}, context: BRC<ED>): Promise<Partial<ED["oauthApplication"]["Schema"]>>;
}, context: BRC<ED>): Promise<{
data: Partial<ED["oauthApplication"]["Schema"]>;
alreadyAuth: boolean;
}>;
export declare function createOAuthState<ED extends EntityDict>(params: {
providerId: string;
userId?: string;

View File

@ -184,6 +184,7 @@ export async function getOAuthClientInfo(params, context) {
const { client_id } = params;
const closeRootMode = context.openRootMode();
const systemId = context.getSystemId();
const applicationId = context.getApplicationId();
const [oauthApp] = await context.select("oauthApplication", {
data: {
id: 1,
@ -199,11 +200,41 @@ export async function getOAuthClientInfo(params, context) {
ableState: "enabled",
}
}, {});
// 如果还有正在生效的授权,说明已经授权过了
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
}
},
code: {
oauthApp: {
id: client_id
},
applicationId: applicationId
}
}
}, {});
if (!oauthApp) {
throw new OakUserException('未经授权的客户端应用');
}
closeRootMode();
return oauthApp;
return {
data: oauthApp,
alreadyAuth: !!hasAuth,
};
}
export async function createOAuthState(params, context) {
const { providerId, userId, type } = params;

View File

@ -96,8 +96,12 @@ export default OakComponent({
}
else {
this.setState({
clientInfo: clientInfo.result,
clientInfo: clientInfo.result.data,
});
if (clientInfo.result.alreadyAuth) {
// 已经授权过,直接跳转
this.handleGrant();
}
}
}).catch((err) => {
console.error('Error loading OAuth client info:', err);

View File

@ -301,6 +301,82 @@ const refreshTokenEndpoint = {
};
}
};
const oauthRevocationEndpoint = {
name: "撤销OAuth令牌",
params: [],
method: 'post',
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const { token, token_type_hint } = body;
const { authorization } = header;
// 1. 验证请求参数
if (!token) {
return {
statusCode: 400,
data: { error: "invalid_request", error_description: "Missing token parameter", success: false }
};
}
// 2. 客户端认证(使用 Basic Auth
const decodedAuth = Buffer.from((authorization || '').split(' ')[1] || '', 'base64').toString('utf-8');
const [client_id, client_secret] = decodedAuth.split(':');
if (!client_id || !client_secret) {
return {
statusCode: 401,
data: { error: "invalid_client", error_description: "Missing client credentials", success: false }
};
}
const context = await contextBuilder();
const [oauthApp] = await context.select("oauthApplication", {
data: { id: 1 },
filter: { clientSecret: client_secret, id: client_id }
}, {});
if (!oauthApp) {
await context.commit();
return {
statusCode: 401,
data: { error: "invalid_client", error_description: "Client authentication failed", success: false }
};
}
// 3. 查找令牌记录
let tokenRecord = null;
const tokenProjection = {
data: { id: 1, code: { oauthAppId: 1 } },
filter: {}
};
// 尝试查找 Refresh Token
if (!token_type_hint || token_type_hint === 'refresh_token') {
tokenProjection.filter = { refreshToken: token, code: { oauthAppId: oauthApp.id } };
[tokenRecord] = await context.select("oauthToken", tokenProjection, {});
}
// 如果没找到,且 hint 不是 'refresh_token',则尝试查找 Access Token
if (!tokenRecord && (!token_type_hint || token_type_hint === 'access_token')) {
tokenProjection.filter = { accessToken: token, code: { oauthAppId: oauthApp.id } };
[tokenRecord] = await context.select("oauthToken", tokenProjection, {});
}
// 4. 撤销操作(无论找到与否,都返回 200但如果找到则执行失效操作
if (tokenRecord) {
const pastTime = Date.now() - 1000;
// 将 Access Token 和 Refresh Token 的过期时间都设为过去,使其立即失效
await context.operate("oauthToken", {
id: await generateNewIdAsync(),
action: "update",
data: {
accessExpiresAt: pastTime,
refreshExpiresAt: pastTime,
},
filter: {
id: tokenRecord.id,
}
}, {});
}
await context.commit();
// 5. RFC 7009 规定:令牌撤销成功或令牌无效时,返回 HTTP 200
return {
statusCode: 200,
data: {}
};
}
};
const endpoints = {
'oauth/access_token': oauthTokenEndpoint,
'oauth/userinfo': oauthUserInfoEndpoint,

View File

@ -1,2 +1,2 @@
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>>)[];
export default _default;

View File

@ -720,6 +720,9 @@ export type AspectDict<ED extends EntityDict> = {
*/
getOAuthClientInfo: (params: {
client_id: string;
}, context: BackendRuntimeContext<ED>) => Promise<EntityDict['oauthApplication']['Schema'] | null>;
}, context: BackendRuntimeContext<ED>) => Promise<{
data: EntityDict['oauthApplication']['Schema'] | null;
alreadyAuth: boolean;
}>;
};
export default AspectDict;

View File

@ -8,7 +8,10 @@ export declare function loginByOauth<ED extends EntityDict>(params: {
}, context: BRC<ED>): Promise<string>;
export declare function getOAuthClientInfo<ED extends EntityDict>(params: {
client_id: string;
}, context: BRC<ED>): Promise<Partial<ED["oauthApplication"]["Schema"]>>;
}, context: BRC<ED>): Promise<{
data: Partial<ED["oauthApplication"]["Schema"]>;
alreadyAuth: boolean;
}>;
export declare function createOAuthState<ED extends EntityDict>(params: {
providerId: string;
userId?: string;

View File

@ -191,6 +191,7 @@ async function getOAuthClientInfo(params, context) {
const { client_id } = params;
const closeRootMode = context.openRootMode();
const systemId = context.getSystemId();
const applicationId = context.getApplicationId();
const [oauthApp] = await context.select("oauthApplication", {
data: {
id: 1,
@ -206,11 +207,41 @@ async function getOAuthClientInfo(params, context) {
ableState: "enabled",
}
}, {});
// 如果还有正在生效的授权,说明已经授权过了
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
}
},
code: {
oauthApp: {
id: client_id
},
applicationId: applicationId
}
}
}, {});
if (!oauthApp) {
throw new types_1.OakUserException('未经授权的客户端应用');
}
closeRootMode();
return oauthApp;
return {
data: oauthApp,
alreadyAuth: !!hasAuth,
};
}
async function createOAuthState(params, context) {
const { providerId, userId, type } = params;

View File

@ -304,6 +304,82 @@ const refreshTokenEndpoint = {
};
}
};
const oauthRevocationEndpoint = {
name: "撤销OAuth令牌",
params: [],
method: 'post',
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const { token, token_type_hint } = body;
const { authorization } = header;
// 1. 验证请求参数
if (!token) {
return {
statusCode: 400,
data: { error: "invalid_request", error_description: "Missing token parameter", success: false }
};
}
// 2. 客户端认证(使用 Basic Auth
const decodedAuth = Buffer.from((authorization || '').split(' ')[1] || '', 'base64').toString('utf-8');
const [client_id, client_secret] = decodedAuth.split(':');
if (!client_id || !client_secret) {
return {
statusCode: 401,
data: { error: "invalid_client", error_description: "Missing client credentials", success: false }
};
}
const context = await contextBuilder();
const [oauthApp] = await context.select("oauthApplication", {
data: { id: 1 },
filter: { clientSecret: client_secret, id: client_id }
}, {});
if (!oauthApp) {
await context.commit();
return {
statusCode: 401,
data: { error: "invalid_client", error_description: "Client authentication failed", success: false }
};
}
// 3. 查找令牌记录
let tokenRecord = null;
const tokenProjection = {
data: { id: 1, code: { oauthAppId: 1 } },
filter: {}
};
// 尝试查找 Refresh Token
if (!token_type_hint || token_type_hint === 'refresh_token') {
tokenProjection.filter = { refreshToken: token, code: { oauthAppId: oauthApp.id } };
[tokenRecord] = await context.select("oauthToken", tokenProjection, {});
}
// 如果没找到,且 hint 不是 'refresh_token',则尝试查找 Access Token
if (!tokenRecord && (!token_type_hint || token_type_hint === 'access_token')) {
tokenProjection.filter = { accessToken: token, code: { oauthAppId: oauthApp.id } };
[tokenRecord] = await context.select("oauthToken", tokenProjection, {});
}
// 4. 撤销操作(无论找到与否,都返回 200但如果找到则执行失效操作
if (tokenRecord) {
const pastTime = Date.now() - 1000;
// 将 Access Token 和 Refresh Token 的过期时间都设为过去,使其立即失效
await context.operate("oauthToken", {
id: await (0, uuid_1.generateNewIdAsync)(),
action: "update",
data: {
accessExpiresAt: pastTime,
refreshExpiresAt: pastTime,
},
filter: {
id: tokenRecord.id,
}
}, {});
}
await context.commit();
// 5. RFC 7009 规定:令牌撤销成功或令牌无效时,返回 HTTP 200
return {
statusCode: 200,
data: {}
};
}
};
const endpoints = {
'oauth/access_token': oauthTokenEndpoint,
'oauth/userinfo': oauthUserInfoEndpoint,

View File

@ -1,2 +1,2 @@
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>>)[];
export default _default;

View File

@ -986,9 +986,13 @@ export type AspectDict<ED extends EntityDict> = {
getOAuthClientInfo: (
params: {
client_id: string;
currentUserId?: string;
},
context: BackendRuntimeContext<ED>
) => Promise<EntityDict['oauthApplication']['Schema'] | null>;
) => Promise<{
data: EntityDict['oauthApplication']['Schema'] | null;
alreadyAuth: boolean;
}>;
};
export default AspectDict;

View File

@ -228,10 +228,12 @@ export async function loginByOauth<ED extends EntityDict>(params: {
}
export async function getOAuthClientInfo<ED extends EntityDict>(params: {
client_id: string;
currentUserId?: string;
}, context: BRC<ED>) {
const { client_id } = params;
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,
@ -248,12 +250,50 @@ export async function getOAuthClientInfo<ED extends EntityDict>(params: {
}
}, {})
// 如果还有正在生效的授权,说明已经授权过了
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,
}
}
}, {})
if (hasAuth) {
console.log("用户已有授权记录:", currentUserId, hasAuth.id, hasAuth.userId, hasAuth.applicationId, hasAuth.usageState);
}
if (!oauthApp) {
throw new OakUserException('未经授权的客户端应用');
}
closeRootMode();
return oauthApp;
return {
data: oauthApp,
alreadyAuth: !!hasAuth,
};
}
export async function createOAuthState<ED extends EntityDict>(params: {
providerId: string;
@ -325,7 +365,7 @@ export async function authorize<ED extends EntityDict>(params: {
id: recordId,
userId: context.getCurrentUserId()!,
applicationId: oauthApp.id,
usageState: action === 'grant' ? 'granted' : 'denied',
usageState: action === 'grant' ? 'unused' : 'denied',
authorizedAt: Date.now(),
}
}, {})

View File

@ -108,26 +108,33 @@ export default OakComponent({
this.features.cache.exec("getOAuthClientInfo", {
client_id: clientId,
currentUserId: userId,
}).then((clientInfo) => {
if (!clientInfo.result) {
this.setState({ loading: false });
this.setState({
hasError: true,
errorMsg: 'oauth.authorize.error.invalid_client_id',
});
} else {
this.setState({
clientInfo: clientInfo.result as any,
clientInfo: clientInfo.result.data as any,
});
if (clientInfo.result.alreadyAuth) {
// 已经授权过,直接跳转
this.handleGrant();
} else {
this.setState({ loading: false });
}
}
}).catch((err: Error) => {
this.setState({ loading: false });
console.error('Error loading OAuth client info:', err);
this.setState({
hasError: true,
errorMsg: err.message || 'oauth.authorize.error.unknown',
});
}).finally(() => {
this.setState({ loading: false });
});
})
},
},
methods: {

View File

@ -103,11 +103,125 @@ const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>
};
}
// 如果userIdcode里面的appId都一样则说明是这个用户在这个应用下第二次授权的直接返回
const [existingToken] = await context.select("oauthToken", {
data: {
id: 1,
accessToken: 1,
refreshToken: 1,
accessExpiresAt: 1,
refreshExpiresAt: 1,
},
filter: {
userId: authCodeRecord.userId,
code: {
oauthAppId: app.id,
},
revokedAt: {
$exists: false,
}
}
}, {})
// 如果存在且未过期,直接返回
if (existingToken && (existingToken.accessExpiresAt as number > Date.now())) {
console.log("Existing valid token found, returning it directly");
// 刷新最后一次使用
await context.operate("oauthToken", {
id: await generateNewIdAsync(),
action: "update",
data: {
lastUsedAt: Date.now(),
},
filter: {
id: existingToken.id,
}
}, {})
// // 创建记录
// await context.operate("oauthUserAuthorization", {
// id: await generateNewIdAsync(),
// action: "update",
// data: {
// tokenId: existingToken.id,
// usageState: 'granted',
// },
// filter: {
// codeId: authCodeRecord.id,
// }
// }, {})
await context.commit();
return {
statusCode: 200,
data: {
access_token: existingToken.accessToken,
token_type: "Bearer",
expires_in: (existingToken.accessExpiresAt as number) - Date.now(),
refresh_token: existingToken.refreshToken,
refresh_expires_in: (existingToken.refreshExpiresAt as number) - Date.now(),
success: true,
}
};
}
const expiresIn = 3600; // 1 hour
const refreshTokenExpiresIn = 86400 * 30; // 30 days
// 如果过期就顺带刷新了
if (existingToken && (existingToken.refreshExpiresAt as number < Date.now())) {
console.log("Existing token expired, refreshing it");
const newAccessToken = randomUUID();
const newRefreshToken = randomUUID();
await context.operate("oauthToken", {
id: await generateNewIdAsync(),
action: "update",
data: {
lastUsedAt: Date.now(),
accessToken: newAccessToken,
refreshToken: newRefreshToken,
accessExpiresAt: Date.now() + expiresIn * 1000,
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
}
}, {})
// // 创建记录
// await context.operate("oauthUserAuthorization", {
// id: await generateNewIdAsync(),
// action: "update",
// data: {
// tokenId: existingToken.id,
// usageState: 'granted',
// },
// filter: {
// codeId: authCodeRecord.id,
// }
// }, {})
await context.commit();
return {
statusCode: 200,
data: {
access_token: newAccessToken,
token_type: "Bearer",
expires_in: expiresIn,
refresh_token: newRefreshToken,
refresh_expires_in: refreshTokenExpiresIn,
success: true,
}
};
}
// 有一种情况是access和refresh都过期了但是用户又重新用code来换token
// 这种情况下不能刷新老的,也要当作新的处理
// 创建accessToken
const genaccessToken = randomUUID();
const expiresIn = 3600; // 1 hour
const refreshToken = randomUUID();
const refreshTokenExpiresIn = 86400 * 30; // 30 days
console.log("Creating new access token and refresh token");
// create
const tokenId = await generateNewIdAsync();
@ -131,6 +245,7 @@ const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>
action: "update",
data: {
tokenId: tokenId,
usageState: 'granted',
},
filter: {
codeId: authCodeRecord.id,
@ -339,6 +454,96 @@ const refreshTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDic
}
}
const oauthRevocationEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
name: "撤销OAuth令牌",
params: [],
method: 'post',
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const { token, token_type_hint } = body as { token?: string, token_type_hint?: 'access_token' | 'refresh_token' };
const { authorization } = header;
// 1. 验证请求参数
if (!token) {
return {
statusCode: 400,
data: { error: "invalid_request", error_description: "Missing token parameter", success: false }
}
}
// 2. 客户端认证(使用 Basic Auth
const decodedAuth = Buffer.from((authorization || '').split(' ')[1] || '', 'base64').toString('utf-8');
const [client_id, client_secret] = decodedAuth.split(':');
if (!client_id || !client_secret) {
return {
statusCode: 401,
data: { error: "invalid_client", error_description: "Missing client credentials", success: false }
}
}
const context = await contextBuilder();
const [oauthApp] = await context.select("oauthApplication", {
data: { id: 1 },
filter: { clientSecret: client_secret, id: client_id }
}, {});
if (!oauthApp) {
await context.commit();
return {
statusCode: 401,
data: { error: "invalid_client", error_description: "Client authentication failed", success: false }
}
}
// 3. 查找令牌记录
let tokenRecord = null;
const tokenProjection = {
data: { id: 1, code: { oauthAppId: 1 } },
filter: {}
};
// 尝试查找 Refresh Token
if (!token_type_hint || token_type_hint === 'refresh_token') {
tokenProjection.filter = { refreshToken: token, code: { oauthAppId: oauthApp.id } };
[tokenRecord] = await context.select("oauthToken", tokenProjection, {});
}
// 如果没找到,且 hint 不是 'refresh_token',则尝试查找 Access Token
if (!tokenRecord && (!token_type_hint || token_type_hint === 'access_token')) {
tokenProjection.filter = { accessToken: token, code: { oauthAppId: oauthApp.id } };
[tokenRecord] = await context.select("oauthToken", tokenProjection, {});
}
// 4. 撤销操作(无论找到与否,都返回 200但如果找到则执行失效操作
if (tokenRecord) {
const pastTime = Date.now() - 1000;
// 将 Access Token 和 Refresh Token 的过期时间都设为过去,使其立即失效
await context.operate("oauthToken", {
id: await generateNewIdAsync(),
action: "update",
data: {
accessExpiresAt: pastTime,
refreshExpiresAt: pastTime,
},
filter: {
id: tokenRecord.id,
}
}, {});
}
await context.commit();
// 5. RFC 7009 规定:令牌撤销成功或令牌无效时,返回 HTTP 200
return {
statusCode: 200,
data: {}
};
}
}
const endpoints: Record<string, Endpoint<EntityDict, BRC<EntityDict>>> = {
'oauth/access_token': oauthTokenEndpoint,
'oauth/userinfo': oauthUserInfoEndpoint,

View File

@ -20,13 +20,25 @@ export interface Schema extends EntityShape {
token?: OauthToken; // 关联的令牌(在调用接口之后生成)
};
export type UsageState = 'granted' | 'denied' | 'revoked';
export type UsageAction = 'revoke';
// 1. 未使用
// 用户已经选择授权,但是还未颁发令牌 (有可能是复用了之前的授权)
// 2. 已授权
// 用户已经授权,并且颁发了令牌
// 3. 已拒绝
// 用户拒绝了授权请求
// 4. 已撤销
// 用户撤销了之前的授权
export type UsageState = 'unused' | 'granted' | 'denied' | 'revoked';
// 用户授权记录的操作
// 用户可以授权或者撤销授权
export type UsageAction = 'revoke' | 'award' | 'deny';
export type Action = UsageAction;
export const UsageActionDef: ActionDef<UsageAction, UsageState> = {
stm: {
revoke: ['granted', "revoked"]
revoke: ['granted', "revoked"],
award: ['unused', 'granted'],
deny: [['unused', 'granted'], 'denied'],
}
};
@ -47,12 +59,15 @@ export const entityDesc: EntityDesc<Schema, Action, '', {
},
action: {
revoke: '撤销授权',
award: '授权',
deny: '拒绝授权',
},
v: {
usageState: {
granted: '已授权',
denied: '未授权',
revoked: '已撤销',
unused: '未使用',
}
}
},
@ -66,6 +81,7 @@ export const entityDesc: EntityDesc<Schema, Action, '', {
granted: '#28a745',
denied: '#dc3545',
revoked: '#6c757d',
unused: '#ffc107',
}
}
},