feat: 如果是在某一应用下反复授权则直接允许,并修改实体添加了更多认真状态
This commit is contained in:
parent
9a05b713ab
commit
0dc7dd4d98
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}, {})
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -103,11 +103,125 @@ const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>
|
|||
};
|
||||
}
|
||||
|
||||
// 如果userId,code里面的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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue