fix: 修复oauth-endpoint相关问题,并允许重复注册处理器
This commit is contained in:
parent
ba434da707
commit
a3ec5fc808
|
|
@ -26,271 +26,284 @@ 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, code_verifier // PKCE支持
|
try {
|
||||||
} = body;
|
const { client_id, client_secret, grant_type, code, redirect_uri, code_verifier // PKCE支持
|
||||||
const [app] = await context.select("oauthApplication", {
|
} = body;
|
||||||
data: {
|
const [app] = await context.select("oauthApplication", {
|
||||||
id: 1,
|
|
||||||
clientSecret: 1,
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
id: client_id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
if (!app) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_client", error_description: "Client not found", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (app.clientSecret !== client_secret) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_client", error_description: "Client secret mismatch", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// grant_type几种类型, 目前只支持authorization_code
|
|
||||||
if (grant_type !== "authorization_code") {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "unsupported_grant_type", error_description: "Only authorization_code grant type is supported", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 找code的记录
|
|
||||||
const [authCodeRecord] = await context.select("oauthAuthorizationCode", {
|
|
||||||
data: {
|
|
||||||
id: 1,
|
|
||||||
code: 1,
|
|
||||||
redirectUri: 1,
|
|
||||||
userId: 1,
|
|
||||||
expiresAt: 1,
|
|
||||||
usedAt: 1,
|
|
||||||
codeChallenge: 1,
|
|
||||||
codeChallengeMethod: 1,
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
code,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
// 找不到记录
|
|
||||||
if (!authCodeRecord) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// PKCE 验证
|
|
||||||
if (authCodeRecord.codeChallenge) {
|
|
||||||
if (!code_verifier) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: {
|
|
||||||
error: "invalid_request",
|
|
||||||
error_description: "code_verifier is required",
|
|
||||||
success: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 验证 code_verifier
|
|
||||||
const isValid = await verifyPKCE(code_verifier, authCodeRecord.codeChallenge, authCodeRecord.codeChallengeMethod);
|
|
||||||
if (!isValid) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: {
|
|
||||||
error: "invalid_grant",
|
|
||||||
error_description: "Invalid code_verifier",
|
|
||||||
success: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 验证redirect_uri
|
|
||||||
if (authCodeRecord.redirectUri !== redirect_uri) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_grant", error_description: "Redirect URI mismatch", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 验证过期
|
|
||||||
if (authCodeRecord.expiresAt < Date.now()) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_grant", error_description: "Authorization code expired", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 验证是否已使用
|
|
||||||
if (authCodeRecord.usedAt) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_grant", error_description: "Authorization code already used", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 如果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 > Date.now())) {
|
|
||||||
console.log("Existing valid token found, returning it directly");
|
|
||||||
// 刷新最后一次使用
|
|
||||||
await context.operate("oauthToken", {
|
|
||||||
id: await generateNewIdAsync(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
data: {
|
||||||
lastUsedAt: Date.now(),
|
id: 1,
|
||||||
|
clientSecret: 1,
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
id: existingToken.id,
|
id: client_id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, {});
|
||||||
// // 创建记录
|
if (!app) {
|
||||||
// await context.operate("oauthUserAuthorization", {
|
await context.commit();
|
||||||
// id: await generateNewIdAsync(),
|
return {
|
||||||
// action: "update",
|
statusCode: 400,
|
||||||
// data: {
|
data: { error: "invalid_client", error_description: "Client not found", success: false }
|
||||||
// tokenId: existingToken.id,
|
};
|
||||||
// usageState: 'granted',
|
}
|
||||||
// },
|
if (app.clientSecret !== client_secret) {
|
||||||
// filter: {
|
await context.commit();
|
||||||
// codeId: authCodeRecord.id,
|
return {
|
||||||
// }
|
statusCode: 400,
|
||||||
// }, {})
|
data: { error: "invalid_client", error_description: "Client secret mismatch", success: false }
|
||||||
await context.commit();
|
};
|
||||||
return {
|
}
|
||||||
statusCode: 200,
|
// grant_type几种类型, 目前只支持authorization_code
|
||||||
|
if (grant_type !== "authorization_code") {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "unsupported_grant_type", error_description: "Only authorization_code grant type is supported", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 找code的记录
|
||||||
|
const [authCodeRecord] = await context.select("oauthAuthorizationCode", {
|
||||||
data: {
|
data: {
|
||||||
access_token: existingToken.accessToken,
|
id: 1,
|
||||||
token_type: "Bearer",
|
code: 1,
|
||||||
expires_in: existingToken.accessExpiresAt - Date.now(),
|
redirectUri: 1,
|
||||||
refresh_token: existingToken.refreshToken,
|
userId: 1,
|
||||||
refresh_expires_in: existingToken.refreshExpiresAt - Date.now(),
|
expiresAt: 1,
|
||||||
success: true,
|
usedAt: 1,
|
||||||
|
codeChallenge: 1,
|
||||||
|
codeChallengeMethod: 1,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
code,
|
||||||
}
|
}
|
||||||
};
|
}, {});
|
||||||
}
|
// 找不到记录
|
||||||
const expiresIn = 3600; // 1 hour
|
if (!authCodeRecord) {
|
||||||
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
await context.commit();
|
||||||
// 如果过期就顺带刷新了(refresh token没过期, accessToken过期了)
|
return {
|
||||||
if (existingToken && (existingToken.refreshExpiresAt > Date.now())) {
|
statusCode: 400,
|
||||||
console.log("Existing token expired, refreshing it");
|
data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false }
|
||||||
const newAccessToken = randomUUID();
|
};
|
||||||
const newRefreshToken = randomUUID();
|
}
|
||||||
|
// PKCE 验证
|
||||||
|
if (authCodeRecord.codeChallenge) {
|
||||||
|
if (!code_verifier) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: {
|
||||||
|
error: "invalid_request",
|
||||||
|
error_description: "code_verifier is required",
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 验证 code_verifier
|
||||||
|
const isValid = await verifyPKCE(code_verifier, authCodeRecord.codeChallenge, authCodeRecord.codeChallengeMethod);
|
||||||
|
if (!isValid) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: {
|
||||||
|
error: "invalid_grant",
|
||||||
|
error_description: "Invalid code_verifier",
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 验证redirect_uri
|
||||||
|
if (authCodeRecord.redirectUri !== redirect_uri) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Redirect URI mismatch", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 验证过期
|
||||||
|
if (authCodeRecord.expiresAt < Date.now()) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Authorization code expired", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 验证是否已使用
|
||||||
|
if (authCodeRecord.usedAt) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Authorization code already used", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 如果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 > 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 - Date.now(),
|
||||||
|
refresh_token: existingToken.refreshToken,
|
||||||
|
refresh_expires_in: existingToken.refreshExpiresAt - Date.now(),
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const expiresIn = 3600; // 1 hour
|
||||||
|
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
||||||
|
// 如果过期就顺带刷新了(refresh token没过期, accessToken过期了)
|
||||||
|
if (existingToken && (existingToken.refreshExpiresAt > 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,
|
||||||
|
},
|
||||||
|
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: 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 refreshToken = randomUUID();
|
||||||
|
console.log("Creating new access token and refresh token");
|
||||||
|
// create
|
||||||
|
const tokenId = await generateNewIdAsync();
|
||||||
await context.operate("oauthToken", {
|
await context.operate("oauthToken", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "create",
|
||||||
|
data: {
|
||||||
|
id: tokenId,
|
||||||
|
accessToken: genaccessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
userId: authCodeRecord.userId,
|
||||||
|
accessExpiresAt: Date.now() + expiresIn * 1000,
|
||||||
|
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
||||||
|
codeId: authCodeRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
// 创建记录
|
||||||
|
await context.operate("oauthUserAuthorization", {
|
||||||
id: await generateNewIdAsync(),
|
id: await generateNewIdAsync(),
|
||||||
action: "update",
|
action: "update",
|
||||||
data: {
|
data: {
|
||||||
lastUsedAt: Date.now(),
|
tokenId: tokenId,
|
||||||
accessToken: newAccessToken,
|
usageState: 'granted',
|
||||||
refreshToken: newRefreshToken,
|
},
|
||||||
accessExpiresAt: Date.now() + expiresIn * 1000,
|
filter: {
|
||||||
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
codeId: authCodeRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
// 标记code为已使用
|
||||||
|
await context.operate("oauthAuthorizationCode", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
usedAt: Date.now(),
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: authCodeRecord.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, {});
|
||||||
// // 创建记录
|
|
||||||
// await context.operate("oauthUserAuthorization", {
|
|
||||||
// id: await generateNewIdAsync(),
|
|
||||||
// action: "update",
|
|
||||||
// data: {
|
|
||||||
// tokenId: existingToken.id,
|
|
||||||
// usageState: 'granted',
|
|
||||||
// },
|
|
||||||
// filter: {
|
|
||||||
// codeId: authCodeRecord.id,
|
|
||||||
// }
|
|
||||||
// }, {})
|
|
||||||
await context.commit();
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
data: {
|
data: {
|
||||||
access_token: newAccessToken,
|
access_token: genaccessToken,
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
expires_in: expiresIn,
|
expires_in: expiresIn,
|
||||||
refresh_token: newRefreshToken,
|
refresh_token: refreshToken,
|
||||||
refresh_expires_in: refreshTokenExpiresIn,
|
refresh_expires_in: refreshTokenExpiresIn,
|
||||||
success: true,
|
success: true,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 有一种情况是access和refresh都过期了,但是用户又重新用code来换token
|
catch (err) {
|
||||||
// 这种情况下不能刷新老的,也要当作新的处理
|
console.error("Error in oauth token endpoint:", err);
|
||||||
// 创建accessToken
|
await context.rollback();
|
||||||
const genaccessToken = randomUUID();
|
return {
|
||||||
const refreshToken = randomUUID();
|
statusCode: 500,
|
||||||
console.log("Creating new access token and refresh token");
|
data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false }
|
||||||
// create
|
};
|
||||||
const tokenId = await generateNewIdAsync();
|
}
|
||||||
await context.operate("oauthToken", {
|
|
||||||
id: await generateNewIdAsync(),
|
|
||||||
action: "create",
|
|
||||||
data: {
|
|
||||||
id: tokenId,
|
|
||||||
accessToken: genaccessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
userId: authCodeRecord.userId,
|
|
||||||
accessExpiresAt: Date.now() + expiresIn * 1000,
|
|
||||||
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
|
||||||
codeId: authCodeRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
// 创建记录
|
|
||||||
await context.operate("oauthUserAuthorization", {
|
|
||||||
id: await generateNewIdAsync(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
|
||||||
tokenId: tokenId,
|
|
||||||
usageState: 'granted',
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
codeId: authCodeRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
// 标记code为已使用
|
|
||||||
await context.operate("oauthAuthorizationCode", {
|
|
||||||
id: await generateNewIdAsync(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
|
||||||
usedAt: Date.now(),
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
id: authCodeRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
data: {
|
|
||||||
access_token: genaccessToken,
|
|
||||||
token_type: "Bearer",
|
|
||||||
expires_in: expiresIn,
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
refresh_expires_in: refreshTokenExpiresIn,
|
|
||||||
success: true,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const oauthUserInfoEndpoint = {
|
const oauthUserInfoEndpoint = {
|
||||||
|
|
@ -301,37 +314,47 @@ const oauthUserInfoEndpoint = {
|
||||||
fn: async (contextBuilder, params, header, req, body) => {
|
fn: async (contextBuilder, params, header, req, body) => {
|
||||||
const context = await contextBuilder();
|
const context = await contextBuilder();
|
||||||
const token = header.authorization; // Bearer token
|
const token = header.authorization; // Bearer token
|
||||||
const checkResult = await checkOauthTokenAvaliable(context, token);
|
try {
|
||||||
if (checkResult.error) {
|
const checkResult = await checkOauthTokenAvaliable(context, token);
|
||||||
|
if (checkResult.error) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: checkResult.statusCode || 401,
|
||||||
|
data: { error: checkResult.error, success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const tokenRecord = checkResult.tokenRecord;
|
||||||
|
assert(tokenRecord?.user, "User must be present in token record");
|
||||||
|
assert(tokenRecord?.code?.application, "Application must be present in token record");
|
||||||
|
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
||||||
|
const application = tokenRecord.code.application;
|
||||||
|
let avatarUrl = '';
|
||||||
|
if (extrafile) {
|
||||||
|
avatarUrl = await composeFileUrlBackend(application, extrafile, context);
|
||||||
|
}
|
||||||
await context.commit();
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: checkResult.statusCode || 401,
|
statusCode: 200, data: {
|
||||||
data: { error: checkResult.error, success: false }
|
userInfo: {
|
||||||
|
id: tokenRecord.user.id,
|
||||||
|
name: tokenRecord.user.name,
|
||||||
|
nickname: tokenRecord.user.nickname,
|
||||||
|
birth: tokenRecord.user.birth,
|
||||||
|
gender: tokenRecord.user.gender,
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
},
|
||||||
|
error: null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const tokenRecord = checkResult.tokenRecord;
|
catch (err) {
|
||||||
assert(tokenRecord?.user, "User must be present in token record");
|
console.error("Error in oauth userinfo endpoint:", err);
|
||||||
assert(tokenRecord?.code?.application, "Application must be present in token record");
|
await context.rollback();
|
||||||
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
return {
|
||||||
const application = tokenRecord.code.application;
|
statusCode: 500,
|
||||||
let avatarUrl = '';
|
data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false }
|
||||||
if (extrafile) {
|
};
|
||||||
avatarUrl = await composeFileUrlBackend(application, extrafile, context);
|
|
||||||
}
|
}
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 200, data: {
|
|
||||||
userInfo: {
|
|
||||||
id: tokenRecord.user.id,
|
|
||||||
name: tokenRecord.user.name,
|
|
||||||
nickname: tokenRecord.user.nickname,
|
|
||||||
birth: tokenRecord.user.birth,
|
|
||||||
gender: tokenRecord.user.gender,
|
|
||||||
avatarUrl: avatarUrl,
|
|
||||||
},
|
|
||||||
error: null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const refreshTokenEndpoint = {
|
const refreshTokenEndpoint = {
|
||||||
|
|
@ -366,88 +389,98 @@ const refreshTokenEndpoint = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const context = await contextBuilder();
|
const context = await contextBuilder();
|
||||||
const [oauthApp] = await context.select("oauthApplication", {
|
try {
|
||||||
data: {
|
const [oauthApp] = await context.select("oauthApplication", {
|
||||||
id: 1,
|
data: {
|
||||||
},
|
id: 1,
|
||||||
filter: {
|
},
|
||||||
clientSecret: client_secret,
|
filter: {
|
||||||
id: client_id,
|
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 }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, {});
|
const [tokenRecord] = await context.select("oauthToken", {
|
||||||
if (!oauthApp) {
|
data: {
|
||||||
await context.commit();
|
id: 1,
|
||||||
return {
|
userId: 1,
|
||||||
statusCode: 401,
|
accessToken: 1,
|
||||||
data: { error: "invalid_client", error_description: "Client authentication failed", success: false }
|
accessExpiresAt: 1,
|
||||||
};
|
refreshToken: 1,
|
||||||
}
|
refreshExpiresAt: 1,
|
||||||
const [tokenRecord] = await context.select("oauthToken", {
|
code: {
|
||||||
data: {
|
applicationId: 1,
|
||||||
id: 1,
|
oauthApp: {
|
||||||
userId: 1,
|
id: 1,
|
||||||
accessToken: 1,
|
}
|
||||||
accessExpiresAt: 1,
|
}
|
||||||
refreshToken: 1,
|
},
|
||||||
refreshExpiresAt: 1,
|
filter: {
|
||||||
code: {
|
refreshToken: refresh_token,
|
||||||
applicationId: 1,
|
code: {
|
||||||
oauthApp: {
|
oauthAppId: oauthApp.id,
|
||||||
id: 1,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}, {});
|
||||||
filter: {
|
if (!tokenRecord) {
|
||||||
refreshToken: refresh_token,
|
await context.commit();
|
||||||
code: {
|
return {
|
||||||
oauthAppId: oauthApp.id,
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Invalid refresh token", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (tokenRecord.refreshExpiresAt < Date.now()) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Refresh token expired", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 生成新的令牌
|
||||||
|
const newAccessToken = randomUUID();
|
||||||
|
const newRefreshToken = randomUUID();
|
||||||
|
const expiresIn = 3600; // 1 hour
|
||||||
|
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
||||||
|
await context.operate("oauthToken", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
refreshToken: newRefreshToken,
|
||||||
|
accessExpiresAt: Date.now() + expiresIn * 1000,
|
||||||
|
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: tokenRecord.id,
|
||||||
}
|
}
|
||||||
}
|
}, {});
|
||||||
}, {});
|
|
||||||
if (!tokenRecord) {
|
|
||||||
await context.commit();
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 400,
|
statusCode: 200,
|
||||||
data: { error: "invalid_grant", error_description: "Invalid refresh token", success: false }
|
data: {
|
||||||
|
access_token: newAccessToken,
|
||||||
|
token_type: "Bearer",
|
||||||
|
expires_in: expiresIn,
|
||||||
|
refresh_token: newRefreshToken,
|
||||||
|
refresh_expires_in: refreshTokenExpiresIn,
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (tokenRecord.refreshExpiresAt < Date.now()) {
|
catch (err) {
|
||||||
await context.commit();
|
console.error("Error in refresh token endpoint:", err);
|
||||||
|
await context.rollback();
|
||||||
return {
|
return {
|
||||||
statusCode: 400,
|
statusCode: 500,
|
||||||
data: { error: "invalid_grant", error_description: "Refresh token expired", success: false }
|
data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 生成新的令牌
|
|
||||||
const newAccessToken = randomUUID();
|
|
||||||
const newRefreshToken = randomUUID();
|
|
||||||
const expiresIn = 3600; // 1 hour
|
|
||||||
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
|
||||||
await context.operate("oauthToken", {
|
|
||||||
id: await generateNewIdAsync(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
|
||||||
accessToken: newAccessToken,
|
|
||||||
refreshToken: newRefreshToken,
|
|
||||||
accessExpiresAt: Date.now() + expiresIn * 1000,
|
|
||||||
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
id: tokenRecord.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,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const oauthRevocationEndpoint = {
|
const oauthRevocationEndpoint = {
|
||||||
|
|
@ -475,64 +508,74 @@ const oauthRevocationEndpoint = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const context = await contextBuilder();
|
const context = await contextBuilder();
|
||||||
const [oauthApp] = await context.select("oauthApplication", {
|
try {
|
||||||
data: { id: 1 },
|
const [oauthApp] = await context.select("oauthApplication", {
|
||||||
filter: { clientSecret: client_secret, id: client_id }
|
data: { id: 1 },
|
||||||
}, {});
|
filter: { clientSecret: client_secret, id: client_id }
|
||||||
if (!oauthApp) {
|
}, {});
|
||||||
|
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,
|
||||||
|
// }
|
||||||
|
// }, {});
|
||||||
|
// 使用这个token的认证记录都撤销掉,在trigger里会自动设置 revokedAt
|
||||||
|
await context.operate("oauthUserAuthorization", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "revoke",
|
||||||
|
data: {},
|
||||||
|
filter: {
|
||||||
|
tokenId: tokenRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
await context.commit();
|
await context.commit();
|
||||||
|
// 5. RFC 7009 规定:令牌撤销成功或令牌无效时,返回 HTTP 200
|
||||||
return {
|
return {
|
||||||
statusCode: 401,
|
statusCode: 200,
|
||||||
data: { error: "invalid_client", error_description: "Client authentication failed", success: false }
|
data: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 3. 查找令牌记录
|
catch (err) {
|
||||||
let tokenRecord = null;
|
console.error("Error in oauth token revocation endpoint:", err);
|
||||||
const tokenProjection = {
|
await context.rollback();
|
||||||
data: { id: 1, code: { oauthAppId: 1 } },
|
return {
|
||||||
filter: {}
|
statusCode: 500,
|
||||||
};
|
data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false }
|
||||||
// 尝试查找 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,
|
|
||||||
// }
|
|
||||||
// }, {});
|
|
||||||
// 使用这个token的认证记录都撤销掉,在trigger里会自动设置 revokedAt
|
|
||||||
await context.operate("oauthUserAuthorization", {
|
|
||||||
id: await generateNewIdAsync(),
|
|
||||||
action: "revoke",
|
|
||||||
data: {},
|
|
||||||
filter: {
|
|
||||||
tokenId: tokenRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
await context.commit();
|
|
||||||
// 5. RFC 7009 规定:令牌撤销成功或令牌无效时,返回 HTTP 200
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const endpoints = {
|
const endpoints = {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@ import { applicationProjection, extraFileProjection } from "../../types/Projecti
|
||||||
import { getDefaultHandlers } from "./handler";
|
import { getDefaultHandlers } from "./handler";
|
||||||
const handlerMap = new Map();
|
const handlerMap = new Map();
|
||||||
export const registerOauthUserinfoHandler = (type, handler) => {
|
export const registerOauthUserinfoHandler = (type, handler) => {
|
||||||
if (handlerMap.has(type)) {
|
|
||||||
throw new Error(`oauth provider type ${type} 的 userinfo 处理器已注册`);
|
|
||||||
}
|
|
||||||
handlerMap.set(type, handler);
|
handlerMap.set(type, handler);
|
||||||
};
|
};
|
||||||
export const processUserInfo = (type, data) => {
|
export const processUserInfo = (type, data) => {
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ async function loginByOauth(params, context) {
|
||||||
const { user } = existingOAuthUser;
|
const { user } = existingOAuthUser;
|
||||||
const targetUser = user?.userState === 'merged' ? user.ref : user;
|
const targetUser = user?.userState === 'merged' ? user.ref : user;
|
||||||
const tokenValue = await (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', existingOAuthUser.id, // 使用已存在的 oauthUser ID
|
const tokenValue = await (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', existingOAuthUser.id, // 使用已存在的 oauthUser ID
|
||||||
undefined, targetUser // 关联的用户
|
undefined, targetUser // 关联的用户
|
||||||
);
|
);
|
||||||
// 更新登录信息
|
// 更新登录信息
|
||||||
await context.operate("oauthUser", {
|
await context.operate("oauthUser", {
|
||||||
|
|
@ -172,7 +172,7 @@ async function loginByOauth(params, context) {
|
||||||
};
|
};
|
||||||
// 不传 user 参数,会自动创建新用户
|
// 不传 user 参数,会自动创建新用户
|
||||||
const tokenValue = await (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', undefined, oauthUserCreateData, // 创建新的 oauthUser
|
const tokenValue = await (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', undefined, oauthUserCreateData, // 创建新的 oauthUser
|
||||||
undefined // 不传 user,自动创建新用户
|
undefined // 不传 user,自动创建新用户
|
||||||
);
|
);
|
||||||
await context.operate("oauthUser", {
|
await context.operate("oauthUser", {
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
|
@ -367,19 +367,21 @@ async function authorize(params, context) {
|
||||||
throw new Error('unknown action');
|
throw new Error('unknown action');
|
||||||
}
|
}
|
||||||
const fetchOAuthUserInfo = async (code, providerConfig) => {
|
const fetchOAuthUserInfo = async (code, providerConfig) => {
|
||||||
|
const params = {
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: code,
|
||||||
|
client_id: providerConfig.clientId,
|
||||||
|
client_secret: providerConfig.clientSecret,
|
||||||
|
redirect_uri: providerConfig.redirectUri,
|
||||||
|
}
|
||||||
|
console.log("使用 OAuth Code 获取 Access Token:", providerConfig.tokenEndpoint, params);
|
||||||
// 1. 使用 code 换取 access_token
|
// 1. 使用 code 换取 access_token
|
||||||
const tokenResponse = await fetch(providerConfig.tokenEndpoint, {
|
const tokenResponse = await fetch(providerConfig.tokenEndpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams(params),
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code: code,
|
|
||||||
client_id: providerConfig.clientId,
|
|
||||||
client_secret: providerConfig.clientSecret,
|
|
||||||
redirect_uri: providerConfig.redirectUri,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!tokenResponse.ok) {
|
if (!tokenResponse.ok) {
|
||||||
const errorjson = await tokenResponse.json();
|
const errorjson = await tokenResponse.json();
|
||||||
|
|
|
||||||
|
|
@ -29,271 +29,284 @@ 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, code_verifier // PKCE支持
|
try {
|
||||||
} = body;
|
const { client_id, client_secret, grant_type, code, redirect_uri, code_verifier // PKCE支持
|
||||||
const [app] = await context.select("oauthApplication", {
|
} = body;
|
||||||
data: {
|
const [app] = await context.select("oauthApplication", {
|
||||||
id: 1,
|
|
||||||
clientSecret: 1,
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
id: client_id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
if (!app) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_client", error_description: "Client not found", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (app.clientSecret !== client_secret) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_client", error_description: "Client secret mismatch", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// grant_type几种类型, 目前只支持authorization_code
|
|
||||||
if (grant_type !== "authorization_code") {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "unsupported_grant_type", error_description: "Only authorization_code grant type is supported", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 找code的记录
|
|
||||||
const [authCodeRecord] = await context.select("oauthAuthorizationCode", {
|
|
||||||
data: {
|
|
||||||
id: 1,
|
|
||||||
code: 1,
|
|
||||||
redirectUri: 1,
|
|
||||||
userId: 1,
|
|
||||||
expiresAt: 1,
|
|
||||||
usedAt: 1,
|
|
||||||
codeChallenge: 1,
|
|
||||||
codeChallengeMethod: 1,
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
code,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
// 找不到记录
|
|
||||||
if (!authCodeRecord) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// PKCE 验证
|
|
||||||
if (authCodeRecord.codeChallenge) {
|
|
||||||
if (!code_verifier) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: {
|
|
||||||
error: "invalid_request",
|
|
||||||
error_description: "code_verifier is required",
|
|
||||||
success: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 验证 code_verifier
|
|
||||||
const isValid = await verifyPKCE(code_verifier, authCodeRecord.codeChallenge, authCodeRecord.codeChallengeMethod);
|
|
||||||
if (!isValid) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: {
|
|
||||||
error: "invalid_grant",
|
|
||||||
error_description: "Invalid code_verifier",
|
|
||||||
success: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 验证redirect_uri
|
|
||||||
if (authCodeRecord.redirectUri !== redirect_uri) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_grant", error_description: "Redirect URI mismatch", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 验证过期
|
|
||||||
if (authCodeRecord.expiresAt < Date.now()) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_grant", error_description: "Authorization code expired", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 验证是否已使用
|
|
||||||
if (authCodeRecord.usedAt) {
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 400,
|
|
||||||
data: { error: "invalid_grant", error_description: "Authorization code already used", success: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 如果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 > Date.now())) {
|
|
||||||
console.log("Existing valid token found, returning it directly");
|
|
||||||
// 刷新最后一次使用
|
|
||||||
await context.operate("oauthToken", {
|
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
data: {
|
||||||
lastUsedAt: Date.now(),
|
id: 1,
|
||||||
|
clientSecret: 1,
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
id: existingToken.id,
|
id: client_id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, {});
|
||||||
// // 创建记录
|
if (!app) {
|
||||||
// await context.operate("oauthUserAuthorization", {
|
await context.commit();
|
||||||
// id: await generateNewIdAsync(),
|
return {
|
||||||
// action: "update",
|
statusCode: 400,
|
||||||
// data: {
|
data: { error: "invalid_client", error_description: "Client not found", success: false }
|
||||||
// tokenId: existingToken.id,
|
};
|
||||||
// usageState: 'granted',
|
}
|
||||||
// },
|
if (app.clientSecret !== client_secret) {
|
||||||
// filter: {
|
await context.commit();
|
||||||
// codeId: authCodeRecord.id,
|
return {
|
||||||
// }
|
statusCode: 400,
|
||||||
// }, {})
|
data: { error: "invalid_client", error_description: "Client secret mismatch", success: false }
|
||||||
await context.commit();
|
};
|
||||||
return {
|
}
|
||||||
statusCode: 200,
|
// grant_type几种类型, 目前只支持authorization_code
|
||||||
|
if (grant_type !== "authorization_code") {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "unsupported_grant_type", error_description: "Only authorization_code grant type is supported", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 找code的记录
|
||||||
|
const [authCodeRecord] = await context.select("oauthAuthorizationCode", {
|
||||||
data: {
|
data: {
|
||||||
access_token: existingToken.accessToken,
|
id: 1,
|
||||||
token_type: "Bearer",
|
code: 1,
|
||||||
expires_in: existingToken.accessExpiresAt - Date.now(),
|
redirectUri: 1,
|
||||||
refresh_token: existingToken.refreshToken,
|
userId: 1,
|
||||||
refresh_expires_in: existingToken.refreshExpiresAt - Date.now(),
|
expiresAt: 1,
|
||||||
success: true,
|
usedAt: 1,
|
||||||
|
codeChallenge: 1,
|
||||||
|
codeChallengeMethod: 1,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
code,
|
||||||
}
|
}
|
||||||
};
|
}, {});
|
||||||
}
|
// 找不到记录
|
||||||
const expiresIn = 3600; // 1 hour
|
if (!authCodeRecord) {
|
||||||
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
await context.commit();
|
||||||
// 如果过期就顺带刷新了(refresh token没过期, accessToken过期了)
|
return {
|
||||||
if (existingToken && (existingToken.refreshExpiresAt > Date.now())) {
|
statusCode: 400,
|
||||||
console.log("Existing token expired, refreshing it");
|
data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false }
|
||||||
const newAccessToken = (0, crypto_1.randomUUID)();
|
};
|
||||||
const newRefreshToken = (0, crypto_1.randomUUID)();
|
}
|
||||||
|
// PKCE 验证
|
||||||
|
if (authCodeRecord.codeChallenge) {
|
||||||
|
if (!code_verifier) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: {
|
||||||
|
error: "invalid_request",
|
||||||
|
error_description: "code_verifier is required",
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 验证 code_verifier
|
||||||
|
const isValid = await verifyPKCE(code_verifier, authCodeRecord.codeChallenge, authCodeRecord.codeChallengeMethod);
|
||||||
|
if (!isValid) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: {
|
||||||
|
error: "invalid_grant",
|
||||||
|
error_description: "Invalid code_verifier",
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 验证redirect_uri
|
||||||
|
if (authCodeRecord.redirectUri !== redirect_uri) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Redirect URI mismatch", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 验证过期
|
||||||
|
if (authCodeRecord.expiresAt < Date.now()) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Authorization code expired", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 验证是否已使用
|
||||||
|
if (authCodeRecord.usedAt) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Authorization code already used", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 如果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 > Date.now())) {
|
||||||
|
console.log("Existing valid token found, returning it directly");
|
||||||
|
// 刷新最后一次使用
|
||||||
|
await context.operate("oauthToken", {
|
||||||
|
id: await (0, uuid_1.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 - Date.now(),
|
||||||
|
refresh_token: existingToken.refreshToken,
|
||||||
|
refresh_expires_in: existingToken.refreshExpiresAt - Date.now(),
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const expiresIn = 3600; // 1 hour
|
||||||
|
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
||||||
|
// 如果过期就顺带刷新了(refresh token没过期, accessToken过期了)
|
||||||
|
if (existingToken && (existingToken.refreshExpiresAt > Date.now())) {
|
||||||
|
console.log("Existing token expired, refreshing it");
|
||||||
|
const newAccessToken = (0, crypto_1.randomUUID)();
|
||||||
|
const newRefreshToken = (0, crypto_1.randomUUID)();
|
||||||
|
await context.operate("oauthToken", {
|
||||||
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
lastUsedAt: Date.now(),
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
refreshToken: newRefreshToken,
|
||||||
|
accessExpiresAt: Date.now() + expiresIn * 1000,
|
||||||
|
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
||||||
|
},
|
||||||
|
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: newAccessToken,
|
||||||
|
token_type: "Bearer",
|
||||||
|
expires_in: expiresIn,
|
||||||
|
refresh_token: newRefreshToken,
|
||||||
|
refresh_expires_in: refreshTokenExpiresIn,
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 有一种情况是access和refresh都过期了,但是用户又重新用code来换token
|
||||||
|
// 这种情况下不能刷新老的,也要当作新的处理
|
||||||
|
// 创建accessToken
|
||||||
|
const genaccessToken = (0, crypto_1.randomUUID)();
|
||||||
|
const refreshToken = (0, crypto_1.randomUUID)();
|
||||||
|
console.log("Creating new access token and refresh token");
|
||||||
|
// create
|
||||||
|
const tokenId = await (0, uuid_1.generateNewIdAsync)();
|
||||||
await context.operate("oauthToken", {
|
await context.operate("oauthToken", {
|
||||||
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
action: "create",
|
||||||
|
data: {
|
||||||
|
id: tokenId,
|
||||||
|
accessToken: genaccessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
userId: authCodeRecord.userId,
|
||||||
|
accessExpiresAt: Date.now() + expiresIn * 1000,
|
||||||
|
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
||||||
|
codeId: authCodeRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
// 创建记录
|
||||||
|
await context.operate("oauthUserAuthorization", {
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
action: "update",
|
action: "update",
|
||||||
data: {
|
data: {
|
||||||
lastUsedAt: Date.now(),
|
tokenId: tokenId,
|
||||||
accessToken: newAccessToken,
|
usageState: 'granted',
|
||||||
refreshToken: newRefreshToken,
|
},
|
||||||
accessExpiresAt: Date.now() + expiresIn * 1000,
|
filter: {
|
||||||
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
codeId: authCodeRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
// 标记code为已使用
|
||||||
|
await context.operate("oauthAuthorizationCode", {
|
||||||
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
usedAt: Date.now(),
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: authCodeRecord.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, {});
|
||||||
// // 创建记录
|
|
||||||
// await context.operate("oauthUserAuthorization", {
|
|
||||||
// id: await generateNewIdAsync(),
|
|
||||||
// action: "update",
|
|
||||||
// data: {
|
|
||||||
// tokenId: existingToken.id,
|
|
||||||
// usageState: 'granted',
|
|
||||||
// },
|
|
||||||
// filter: {
|
|
||||||
// codeId: authCodeRecord.id,
|
|
||||||
// }
|
|
||||||
// }, {})
|
|
||||||
await context.commit();
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
data: {
|
data: {
|
||||||
access_token: newAccessToken,
|
access_token: genaccessToken,
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
expires_in: expiresIn,
|
expires_in: expiresIn,
|
||||||
refresh_token: newRefreshToken,
|
refresh_token: refreshToken,
|
||||||
refresh_expires_in: refreshTokenExpiresIn,
|
refresh_expires_in: refreshTokenExpiresIn,
|
||||||
success: true,
|
success: true,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 有一种情况是access和refresh都过期了,但是用户又重新用code来换token
|
catch (err) {
|
||||||
// 这种情况下不能刷新老的,也要当作新的处理
|
console.error("Error in oauth token endpoint:", err);
|
||||||
// 创建accessToken
|
await context.rollback();
|
||||||
const genaccessToken = (0, crypto_1.randomUUID)();
|
return {
|
||||||
const refreshToken = (0, crypto_1.randomUUID)();
|
statusCode: 500,
|
||||||
console.log("Creating new access token and refresh token");
|
data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false }
|
||||||
// create
|
};
|
||||||
const tokenId = await (0, uuid_1.generateNewIdAsync)();
|
}
|
||||||
await context.operate("oauthToken", {
|
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
||||||
action: "create",
|
|
||||||
data: {
|
|
||||||
id: tokenId,
|
|
||||||
accessToken: genaccessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
userId: authCodeRecord.userId,
|
|
||||||
accessExpiresAt: Date.now() + expiresIn * 1000,
|
|
||||||
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
|
||||||
codeId: authCodeRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
// 创建记录
|
|
||||||
await context.operate("oauthUserAuthorization", {
|
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
|
||||||
tokenId: tokenId,
|
|
||||||
usageState: 'granted',
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
codeId: authCodeRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
// 标记code为已使用
|
|
||||||
await context.operate("oauthAuthorizationCode", {
|
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
|
||||||
usedAt: Date.now(),
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
id: authCodeRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
data: {
|
|
||||||
access_token: genaccessToken,
|
|
||||||
token_type: "Bearer",
|
|
||||||
expires_in: expiresIn,
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
refresh_expires_in: refreshTokenExpiresIn,
|
|
||||||
success: true,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const oauthUserInfoEndpoint = {
|
const oauthUserInfoEndpoint = {
|
||||||
|
|
@ -304,37 +317,47 @@ const oauthUserInfoEndpoint = {
|
||||||
fn: async (contextBuilder, params, header, req, body) => {
|
fn: async (contextBuilder, params, header, req, body) => {
|
||||||
const context = await contextBuilder();
|
const context = await contextBuilder();
|
||||||
const token = header.authorization; // Bearer token
|
const token = header.authorization; // Bearer token
|
||||||
const checkResult = await (0, oauth_1.checkOauthTokenAvaliable)(context, token);
|
try {
|
||||||
if (checkResult.error) {
|
const checkResult = await (0, oauth_1.checkOauthTokenAvaliable)(context, token);
|
||||||
|
if (checkResult.error) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: checkResult.statusCode || 401,
|
||||||
|
data: { error: checkResult.error, success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const tokenRecord = checkResult.tokenRecord;
|
||||||
|
(0, assert_1.default)(tokenRecord?.user, "User must be present in token record");
|
||||||
|
(0, assert_1.default)(tokenRecord?.code?.application, "Application must be present in token record");
|
||||||
|
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
||||||
|
const application = tokenRecord.code.application;
|
||||||
|
let avatarUrl = '';
|
||||||
|
if (extrafile) {
|
||||||
|
avatarUrl = await (0, index_backend_1.composeFileUrlBackend)(application, extrafile, context);
|
||||||
|
}
|
||||||
await context.commit();
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: checkResult.statusCode || 401,
|
statusCode: 200, data: {
|
||||||
data: { error: checkResult.error, success: false }
|
userInfo: {
|
||||||
|
id: tokenRecord.user.id,
|
||||||
|
name: tokenRecord.user.name,
|
||||||
|
nickname: tokenRecord.user.nickname,
|
||||||
|
birth: tokenRecord.user.birth,
|
||||||
|
gender: tokenRecord.user.gender,
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
},
|
||||||
|
error: null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const tokenRecord = checkResult.tokenRecord;
|
catch (err) {
|
||||||
(0, assert_1.default)(tokenRecord?.user, "User must be present in token record");
|
console.error("Error in oauth userinfo endpoint:", err);
|
||||||
(0, assert_1.default)(tokenRecord?.code?.application, "Application must be present in token record");
|
await context.rollback();
|
||||||
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
return {
|
||||||
const application = tokenRecord.code.application;
|
statusCode: 500,
|
||||||
let avatarUrl = '';
|
data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false }
|
||||||
if (extrafile) {
|
};
|
||||||
avatarUrl = await (0, index_backend_1.composeFileUrlBackend)(application, extrafile, context);
|
|
||||||
}
|
}
|
||||||
await context.commit();
|
|
||||||
return {
|
|
||||||
statusCode: 200, data: {
|
|
||||||
userInfo: {
|
|
||||||
id: tokenRecord.user.id,
|
|
||||||
name: tokenRecord.user.name,
|
|
||||||
nickname: tokenRecord.user.nickname,
|
|
||||||
birth: tokenRecord.user.birth,
|
|
||||||
gender: tokenRecord.user.gender,
|
|
||||||
avatarUrl: avatarUrl,
|
|
||||||
},
|
|
||||||
error: null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const refreshTokenEndpoint = {
|
const refreshTokenEndpoint = {
|
||||||
|
|
@ -369,88 +392,98 @@ const refreshTokenEndpoint = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const context = await contextBuilder();
|
const context = await contextBuilder();
|
||||||
const [oauthApp] = await context.select("oauthApplication", {
|
try {
|
||||||
data: {
|
const [oauthApp] = await context.select("oauthApplication", {
|
||||||
id: 1,
|
data: {
|
||||||
},
|
id: 1,
|
||||||
filter: {
|
},
|
||||||
clientSecret: client_secret,
|
filter: {
|
||||||
id: client_id,
|
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 }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, {});
|
const [tokenRecord] = await context.select("oauthToken", {
|
||||||
if (!oauthApp) {
|
data: {
|
||||||
await context.commit();
|
id: 1,
|
||||||
return {
|
userId: 1,
|
||||||
statusCode: 401,
|
accessToken: 1,
|
||||||
data: { error: "invalid_client", error_description: "Client authentication failed", success: false }
|
accessExpiresAt: 1,
|
||||||
};
|
refreshToken: 1,
|
||||||
}
|
refreshExpiresAt: 1,
|
||||||
const [tokenRecord] = await context.select("oauthToken", {
|
code: {
|
||||||
data: {
|
applicationId: 1,
|
||||||
id: 1,
|
oauthApp: {
|
||||||
userId: 1,
|
id: 1,
|
||||||
accessToken: 1,
|
}
|
||||||
accessExpiresAt: 1,
|
}
|
||||||
refreshToken: 1,
|
},
|
||||||
refreshExpiresAt: 1,
|
filter: {
|
||||||
code: {
|
refreshToken: refresh_token,
|
||||||
applicationId: 1,
|
code: {
|
||||||
oauthApp: {
|
oauthAppId: oauthApp.id,
|
||||||
id: 1,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}, {});
|
||||||
filter: {
|
if (!tokenRecord) {
|
||||||
refreshToken: refresh_token,
|
await context.commit();
|
||||||
code: {
|
return {
|
||||||
oauthAppId: oauthApp.id,
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Invalid refresh token", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (tokenRecord.refreshExpiresAt < Date.now()) {
|
||||||
|
await context.commit();
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
data: { error: "invalid_grant", error_description: "Refresh token expired", success: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 生成新的令牌
|
||||||
|
const newAccessToken = (0, crypto_1.randomUUID)();
|
||||||
|
const newRefreshToken = (0, crypto_1.randomUUID)();
|
||||||
|
const expiresIn = 3600; // 1 hour
|
||||||
|
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
||||||
|
await context.operate("oauthToken", {
|
||||||
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
refreshToken: newRefreshToken,
|
||||||
|
accessExpiresAt: Date.now() + expiresIn * 1000,
|
||||||
|
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: tokenRecord.id,
|
||||||
}
|
}
|
||||||
}
|
}, {});
|
||||||
}, {});
|
|
||||||
if (!tokenRecord) {
|
|
||||||
await context.commit();
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 400,
|
statusCode: 200,
|
||||||
data: { error: "invalid_grant", error_description: "Invalid refresh token", success: false }
|
data: {
|
||||||
|
access_token: newAccessToken,
|
||||||
|
token_type: "Bearer",
|
||||||
|
expires_in: expiresIn,
|
||||||
|
refresh_token: newRefreshToken,
|
||||||
|
refresh_expires_in: refreshTokenExpiresIn,
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (tokenRecord.refreshExpiresAt < Date.now()) {
|
catch (err) {
|
||||||
await context.commit();
|
console.error("Error in refresh token endpoint:", err);
|
||||||
|
await context.rollback();
|
||||||
return {
|
return {
|
||||||
statusCode: 400,
|
statusCode: 500,
|
||||||
data: { error: "invalid_grant", error_description: "Refresh token expired", success: false }
|
data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 生成新的令牌
|
|
||||||
const newAccessToken = (0, crypto_1.randomUUID)();
|
|
||||||
const newRefreshToken = (0, crypto_1.randomUUID)();
|
|
||||||
const expiresIn = 3600; // 1 hour
|
|
||||||
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
|
||||||
await context.operate("oauthToken", {
|
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
|
||||||
accessToken: newAccessToken,
|
|
||||||
refreshToken: newRefreshToken,
|
|
||||||
accessExpiresAt: Date.now() + expiresIn * 1000,
|
|
||||||
refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000,
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
id: tokenRecord.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,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const oauthRevocationEndpoint = {
|
const oauthRevocationEndpoint = {
|
||||||
|
|
@ -478,64 +511,74 @@ const oauthRevocationEndpoint = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const context = await contextBuilder();
|
const context = await contextBuilder();
|
||||||
const [oauthApp] = await context.select("oauthApplication", {
|
try {
|
||||||
data: { id: 1 },
|
const [oauthApp] = await context.select("oauthApplication", {
|
||||||
filter: { clientSecret: client_secret, id: client_id }
|
data: { id: 1 },
|
||||||
}, {});
|
filter: { clientSecret: client_secret, id: client_id }
|
||||||
if (!oauthApp) {
|
}, {});
|
||||||
|
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,
|
||||||
|
// }
|
||||||
|
// }, {});
|
||||||
|
// 使用这个token的认证记录都撤销掉,在trigger里会自动设置 revokedAt
|
||||||
|
await context.operate("oauthUserAuthorization", {
|
||||||
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
action: "revoke",
|
||||||
|
data: {},
|
||||||
|
filter: {
|
||||||
|
tokenId: tokenRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
await context.commit();
|
await context.commit();
|
||||||
|
// 5. RFC 7009 规定:令牌撤销成功或令牌无效时,返回 HTTP 200
|
||||||
return {
|
return {
|
||||||
statusCode: 401,
|
statusCode: 200,
|
||||||
data: { error: "invalid_client", error_description: "Client authentication failed", success: false }
|
data: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 3. 查找令牌记录
|
catch (err) {
|
||||||
let tokenRecord = null;
|
console.error("Error in oauth token revocation endpoint:", err);
|
||||||
const tokenProjection = {
|
await context.rollback();
|
||||||
data: { id: 1, code: { oauthAppId: 1 } },
|
return {
|
||||||
filter: {}
|
statusCode: 500,
|
||||||
};
|
data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false }
|
||||||
// 尝试查找 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,
|
|
||||||
// }
|
|
||||||
// }, {});
|
|
||||||
// 使用这个token的认证记录都撤销掉,在trigger里会自动设置 revokedAt
|
|
||||||
await context.operate("oauthUserAuthorization", {
|
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
||||||
action: "revoke",
|
|
||||||
data: {},
|
|
||||||
filter: {
|
|
||||||
tokenId: tokenRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
await context.commit();
|
|
||||||
// 5. RFC 7009 规定:令牌撤销成功或令牌无效时,返回 HTTP 200
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const endpoints = {
|
const endpoints = {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,6 @@ const Projection_1 = require("../../types/Projection");
|
||||||
const handler_1 = require("./handler");
|
const handler_1 = require("./handler");
|
||||||
const handlerMap = new Map();
|
const handlerMap = new Map();
|
||||||
const registerOauthUserinfoHandler = (type, handler) => {
|
const registerOauthUserinfoHandler = (type, handler) => {
|
||||||
if (handlerMap.has(type)) {
|
|
||||||
throw new Error(`oauth provider type ${type} 的 userinfo 处理器已注册`);
|
|
||||||
}
|
|
||||||
handlerMap.set(type, handler);
|
handlerMap.set(type, handler);
|
||||||
};
|
};
|
||||||
exports.registerOauthUserinfoHandler = registerOauthUserinfoHandler;
|
exports.registerOauthUserinfoHandler = registerOauthUserinfoHandler;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,9 +18,6 @@ export type UserInfoHandler = (data: UserInfo) => UserID | Promise<UserID>
|
||||||
const handlerMap = new Map<EntityDict['oauthProvider']['Schema']['type'], UserInfoHandler>();
|
const handlerMap = new Map<EntityDict['oauthProvider']['Schema']['type'], UserInfoHandler>();
|
||||||
|
|
||||||
export const registerOauthUserinfoHandler = (type: EntityDict['oauthProvider']['Schema']['type'], handler: UserInfoHandler) => {
|
export const registerOauthUserinfoHandler = (type: EntityDict['oauthProvider']['Schema']['type'], handler: UserInfoHandler) => {
|
||||||
if (handlerMap.has(type)) {
|
|
||||||
throw new Error(`oauth provider type ${type} 的 userinfo 处理器已注册`);
|
|
||||||
}
|
|
||||||
handlerMap.set(type, handler);
|
handlerMap.set(type, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +35,6 @@ Object.entries(defaulthandlers).forEach(([type, handler]) => {
|
||||||
registerOauthUserinfoHandler(type as EntityDict['oauthProvider']['Schema']['type'], handler);
|
registerOauthUserinfoHandler(type as EntityDict['oauthProvider']['Schema']['type'], handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function validateToken(token: string | undefined): {
|
function validateToken(token: string | undefined): {
|
||||||
token: string;
|
token: string;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue