From a3ec5fc8087198edf453f131b889d2d02c625a30 Mon Sep 17 00:00:00 2001 From: qcqcqc <1220204124@zust.edu.cn> Date: Wed, 24 Dec 2025 15:13:16 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Doauth-endpoint?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E9=97=AE=E9=A2=98=EF=BC=8C=E5=B9=B6=E5=85=81?= =?UTF-8?q?=E8=AE=B8=E9=87=8D=E5=A4=8D=E6=B3=A8=E5=86=8C=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- es/endpoints/oauth.js | 825 ++++++++++++++++++---------------- es/utils/oauth/index.js | 3 - lib/aspects/oauth.js | 20 +- lib/endpoints/oauth.js | 825 ++++++++++++++++++---------------- lib/utils/oauth/index.js | 3 - src/endpoints/oauth.ts | 937 ++++++++++++++++++++------------------- src/utils/oauth/index.ts | 4 - 7 files changed, 1367 insertions(+), 1250 deletions(-) diff --git a/es/endpoints/oauth.js b/es/endpoints/oauth.js index 2e321c874..3b2ac7cac 100644 --- a/es/endpoints/oauth.js +++ b/es/endpoints/oauth.js @@ -26,271 +26,284 @@ const oauthTokenEndpoint = { type: "free", fn: async (contextBuilder, params, header, req, body) => { const context = await contextBuilder(); - const { client_id, client_secret, grant_type, code, redirect_uri, code_verifier // PKCE支持 - } = body; - const [app] = await context.select("oauthApplication", { - data: { - 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", + try { + const { client_id, client_secret, grant_type, code, redirect_uri, code_verifier // PKCE支持 + } = body; + const [app] = await context.select("oauthApplication", { data: { - lastUsedAt: Date.now(), + id: 1, + clientSecret: 1, }, filter: { - id: existingToken.id, + id: client_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, + 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: { - 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, + id: 1, + code: 1, + redirectUri: 1, + userId: 1, + expiresAt: 1, + usedAt: 1, + codeChallenge: 1, + codeChallengeMethod: 1, + }, + filter: { + code, } - }; - } - 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(); + }, {}); + // 找不到记录 + 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: { + 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", { + 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: { - lastUsedAt: Date.now(), - accessToken: newAccessToken, - refreshToken: newRefreshToken, - accessExpiresAt: Date.now() + expiresIn * 1000, - refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000, + 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.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, + access_token: genaccessToken, token_type: "Bearer", expires_in: expiresIn, - refresh_token: newRefreshToken, + refresh_token: refreshToken, 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", { - 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, - } - }; + catch (err) { + console.error("Error in oauth token endpoint:", err); + await context.rollback(); + return { + statusCode: 500, + data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false } + }; + } } }; const oauthUserInfoEndpoint = { @@ -301,37 +314,47 @@ const oauthUserInfoEndpoint = { fn: async (contextBuilder, params, header, req, body) => { const context = await contextBuilder(); const token = header.authorization; // Bearer token - const checkResult = await checkOauthTokenAvaliable(context, token); - if (checkResult.error) { + try { + 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(); return { - statusCode: checkResult.statusCode || 401, - data: { error: checkResult.error, success: false } + 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 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); + catch (err) { + console.error("Error in oauth userinfo endpoint:", err); + await context.rollback(); + return { + statusCode: 500, + data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false } + }; } - 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 = { @@ -366,88 +389,98 @@ const refreshTokenEndpoint = { }; } const context = await contextBuilder(); - const [oauthApp] = await context.select("oauthApplication", { - data: { - id: 1, - }, - filter: { - clientSecret: client_secret, - id: client_id, + try { + 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 } + }; } - }, {}); - 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", { - data: { - id: 1, - userId: 1, - accessToken: 1, - accessExpiresAt: 1, - refreshToken: 1, - refreshExpiresAt: 1, - code: { - applicationId: 1, - oauthApp: { - id: 1, + const [tokenRecord] = await context.select("oauthToken", { + data: { + id: 1, + userId: 1, + accessToken: 1, + accessExpiresAt: 1, + refreshToken: 1, + refreshExpiresAt: 1, + code: { + applicationId: 1, + oauthApp: { + id: 1, + } + } + }, + filter: { + refreshToken: refresh_token, + code: { + oauthAppId: oauthApp.id, } } - }, - filter: { - refreshToken: refresh_token, - code: { - oauthAppId: oauthApp.id, + }, {}); + if (!tokenRecord) { + await context.commit(); + return { + 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(); return { - statusCode: 400, - data: { error: "invalid_grant", error_description: "Invalid refresh token", success: false } + statusCode: 200, + data: { + access_token: newAccessToken, + token_type: "Bearer", + expires_in: expiresIn, + refresh_token: newRefreshToken, + refresh_expires_in: refreshTokenExpiresIn, + success: true, + } }; } - if (tokenRecord.refreshExpiresAt < Date.now()) { - await context.commit(); + catch (err) { + console.error("Error in refresh token endpoint:", err); + await context.rollback(); return { - statusCode: 400, - data: { error: "invalid_grant", error_description: "Refresh token expired", success: false } + statusCode: 500, + 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 = { @@ -475,64 +508,74 @@ const oauthRevocationEndpoint = { }; } const context = await contextBuilder(); - const [oauthApp] = await context.select("oauthApplication", { - data: { id: 1 }, - filter: { clientSecret: client_secret, id: client_id } - }, {}); - if (!oauthApp) { + try { + 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, + // } + // }, {}); + // 使用这个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: 401, - data: { error: "invalid_client", error_description: "Client authentication failed", success: false } + statusCode: 200, + data: {} }; } - // 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, {}); + catch (err) { + console.error("Error in oauth token revocation endpoint:", err); + await context.rollback(); + return { + statusCode: 500, + data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false } + }; } - // 如果没找到,且 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 = { diff --git a/es/utils/oauth/index.js b/es/utils/oauth/index.js index ecec77076..fb67ed9a2 100644 --- a/es/utils/oauth/index.js +++ b/es/utils/oauth/index.js @@ -3,9 +3,6 @@ import { applicationProjection, extraFileProjection } from "../../types/Projecti import { getDefaultHandlers } from "./handler"; const handlerMap = new Map(); export const registerOauthUserinfoHandler = (type, handler) => { - if (handlerMap.has(type)) { - throw new Error(`oauth provider type ${type} 的 userinfo 处理器已注册`); - } handlerMap.set(type, handler); }; export const processUserInfo = (type, data) => { diff --git a/lib/aspects/oauth.js b/lib/aspects/oauth.js index 14d328545..57a04f278 100644 --- a/lib/aspects/oauth.js +++ b/lib/aspects/oauth.js @@ -127,7 +127,7 @@ async function loginByOauth(params, context) { const { user } = existingOAuthUser; const targetUser = user?.userState === 'merged' ? user.ref : user; const tokenValue = await (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', existingOAuthUser.id, // 使用已存在的 oauthUser ID - undefined, targetUser // 关联的用户 + undefined, targetUser // 关联的用户 ); // 更新登录信息 await context.operate("oauthUser", { @@ -172,7 +172,7 @@ async function loginByOauth(params, context) { }; // 不传 user 参数,会自动创建新用户 const tokenValue = await (0, token_1.setUpTokenAndUser)(env, context, 'oauthUser', undefined, oauthUserCreateData, // 创建新的 oauthUser - undefined // 不传 user,自动创建新用户 + undefined // 不传 user,自动创建新用户 ); await context.operate("oauthUser", { id: await (0, uuid_1.generateNewIdAsync)(), @@ -367,19 +367,21 @@ async function authorize(params, context) { throw new Error('unknown action'); } 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 const tokenResponse = await fetch(providerConfig.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - code: code, - client_id: providerConfig.clientId, - client_secret: providerConfig.clientSecret, - redirect_uri: providerConfig.redirectUri, - }), + body: new URLSearchParams(params), }); if (!tokenResponse.ok) { const errorjson = await tokenResponse.json(); diff --git a/lib/endpoints/oauth.js b/lib/endpoints/oauth.js index 1d76bb320..427647660 100644 --- a/lib/endpoints/oauth.js +++ b/lib/endpoints/oauth.js @@ -29,271 +29,284 @@ const oauthTokenEndpoint = { type: "free", fn: async (contextBuilder, params, header, req, body) => { const context = await contextBuilder(); - const { client_id, client_secret, grant_type, code, redirect_uri, code_verifier // PKCE支持 - } = body; - const [app] = await context.select("oauthApplication", { - data: { - 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", + try { + const { client_id, client_secret, grant_type, code, redirect_uri, code_verifier // PKCE支持 + } = body; + const [app] = await context.select("oauthApplication", { data: { - lastUsedAt: Date.now(), + id: 1, + clientSecret: 1, }, filter: { - id: existingToken.id, + id: client_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, + 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: { - 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, + id: 1, + code: 1, + redirectUri: 1, + userId: 1, + expiresAt: 1, + usedAt: 1, + codeChallenge: 1, + codeChallengeMethod: 1, + }, + filter: { + code, } - }; - } - 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)(); + }, {}); + // 找不到记录 + 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: { + 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", { + 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: { - lastUsedAt: Date.now(), - accessToken: newAccessToken, - refreshToken: newRefreshToken, - accessExpiresAt: Date.now() + expiresIn * 1000, - refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000, + 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.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, + access_token: genaccessToken, token_type: "Bearer", expires_in: expiresIn, - refresh_token: newRefreshToken, + refresh_token: refreshToken, 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", { - 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, - } - }; + catch (err) { + console.error("Error in oauth token endpoint:", err); + await context.rollback(); + return { + statusCode: 500, + data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false } + }; + } } }; const oauthUserInfoEndpoint = { @@ -304,37 +317,47 @@ const oauthUserInfoEndpoint = { fn: async (contextBuilder, params, header, req, body) => { const context = await contextBuilder(); const token = header.authorization; // Bearer token - const checkResult = await (0, oauth_1.checkOauthTokenAvaliable)(context, token); - if (checkResult.error) { + try { + 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(); return { - statusCode: checkResult.statusCode || 401, - data: { error: checkResult.error, success: false } + 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 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); + catch (err) { + console.error("Error in oauth userinfo endpoint:", err); + await context.rollback(); + return { + statusCode: 500, + data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false } + }; } - 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 = { @@ -369,88 +392,98 @@ const refreshTokenEndpoint = { }; } const context = await contextBuilder(); - const [oauthApp] = await context.select("oauthApplication", { - data: { - id: 1, - }, - filter: { - clientSecret: client_secret, - id: client_id, + try { + 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 } + }; } - }, {}); - 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", { - data: { - id: 1, - userId: 1, - accessToken: 1, - accessExpiresAt: 1, - refreshToken: 1, - refreshExpiresAt: 1, - code: { - applicationId: 1, - oauthApp: { - id: 1, + const [tokenRecord] = await context.select("oauthToken", { + data: { + id: 1, + userId: 1, + accessToken: 1, + accessExpiresAt: 1, + refreshToken: 1, + refreshExpiresAt: 1, + code: { + applicationId: 1, + oauthApp: { + id: 1, + } + } + }, + filter: { + refreshToken: refresh_token, + code: { + oauthAppId: oauthApp.id, } } - }, - filter: { - refreshToken: refresh_token, - code: { - oauthAppId: oauthApp.id, + }, {}); + if (!tokenRecord) { + await context.commit(); + return { + 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(); return { - statusCode: 400, - data: { error: "invalid_grant", error_description: "Invalid refresh token", success: false } + statusCode: 200, + data: { + access_token: newAccessToken, + token_type: "Bearer", + expires_in: expiresIn, + refresh_token: newRefreshToken, + refresh_expires_in: refreshTokenExpiresIn, + success: true, + } }; } - if (tokenRecord.refreshExpiresAt < Date.now()) { - await context.commit(); + catch (err) { + console.error("Error in refresh token endpoint:", err); + await context.rollback(); return { - statusCode: 400, - data: { error: "invalid_grant", error_description: "Refresh token expired", success: false } + statusCode: 500, + 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 = { @@ -478,64 +511,74 @@ const oauthRevocationEndpoint = { }; } const context = await contextBuilder(); - const [oauthApp] = await context.select("oauthApplication", { - data: { id: 1 }, - filter: { clientSecret: client_secret, id: client_id } - }, {}); - if (!oauthApp) { + try { + 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, + // } + // }, {}); + // 使用这个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: 401, - data: { error: "invalid_client", error_description: "Client authentication failed", success: false } + statusCode: 200, + data: {} }; } - // 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, {}); + catch (err) { + console.error("Error in oauth token revocation endpoint:", err); + await context.rollback(); + return { + statusCode: 500, + data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false } + }; } - // 如果没找到,且 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 = { diff --git a/lib/utils/oauth/index.js b/lib/utils/oauth/index.js index eda51e06a..3aaf3b1ab 100644 --- a/lib/utils/oauth/index.js +++ b/lib/utils/oauth/index.js @@ -7,9 +7,6 @@ const Projection_1 = require("../../types/Projection"); const handler_1 = require("./handler"); const handlerMap = new Map(); const registerOauthUserinfoHandler = (type, handler) => { - if (handlerMap.has(type)) { - throw new Error(`oauth provider type ${type} 的 userinfo 处理器已注册`); - } handlerMap.set(type, handler); }; exports.registerOauthUserinfoHandler = registerOauthUserinfoHandler; diff --git a/src/endpoints/oauth.ts b/src/endpoints/oauth.ts index 6faf42bab..d972b707f 100644 --- a/src/endpoints/oauth.ts +++ b/src/endpoints/oauth.ts @@ -37,316 +37,328 @@ const oauthTokenEndpoint: Endpoint type: "free", fn: async (contextBuilder, params, header, req, body) => { const context = await contextBuilder() - const { - client_id, - client_secret, - grant_type, - code, - redirect_uri, - code_verifier // PKCE支持 - } = body as { - client_id: string, - client_secret: string, - grant_type: string, - code?: string, - redirect_uri?: string - code_verifier?: string - }; - const [app] = await context.select("oauthApplication", { - data: { - id: 1, - 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: { + try { + const { + client_id, + client_secret, + grant_type, code, - } - }, {}) - - // 找不到记录 - if (!authCodeRecord) { - await context.commit(); - return { - statusCode: 400, - data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false } + redirect_uri, + code_verifier // PKCE支持 + } = body as { + client_id: string, + client_secret: string, + grant_type: string, + code?: string, + redirect_uri?: string + code_verifier?: string }; - } - - // PKCE 验证 - if (authCodeRecord.codeChallenge) { - if (!code_verifier) { - await context.commit(); - return { - statusCode: 400, - data: { - error: "invalid_request", - error_description: "code_verifier is required", - success: false - } - }; - } - - // 验证 code_verifier - const isValid = await verifyPKCE( - code_verifier, - authCodeRecord.codeChallenge as string, - authCodeRecord.codeChallengeMethod as 'plain' | 'S256' - ); - - if (!isValid) { - await context.commit(); - return { - statusCode: 400, - data: { - error: "invalid_grant", - error_description: "Invalid code_verifier", - success: false - } - }; - } - } - - // 验证redirect_uri - if (authCodeRecord.redirectUri !== redirect_uri) { - await context.commit(); - return { - statusCode: 400, - data: { error: "invalid_grant", error_description: "Redirect URI mismatch", success: false } - }; - } - - // 验证过期 - if (authCodeRecord.expiresAt as number < 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 as number > Date.now())) { - console.log("Existing valid token found, returning it directly"); - - // 刷新最后一次使用 - await context.operate("oauthToken", { - id: await generateNewIdAsync(), - action: "update", + const [app] = await context.select("oauthApplication", { data: { - lastUsedAt: Date.now(), + id: 1, + clientSecret: 1, }, filter: { - id: existingToken.id, + id: client_id, } }, {}) - // // 创建记录 - // await context.operate("oauthUserAuthorization", { - // id: await generateNewIdAsync(), - // action: "update", - // data: { - // tokenId: existingToken.id, - // usageState: 'granted', - // }, - // filter: { - // codeId: authCodeRecord.id, - // } - // }, {}) + if (!app) { + await context.commit(); + return { + statusCode: 400, + data: { error: "invalid_client", error_description: "Client not found", success: false } + }; + } - await context.commit(); + if (app.clientSecret !== client_secret) { + await context.commit(); + return { + statusCode: 400, + data: { error: "invalid_client", error_description: "Client secret mismatch", success: false } + }; + } - 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: { - 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, + id: 1, + code: 1, + redirectUri: 1, + userId: 1, + expiresAt: 1, + usedAt: 1, + codeChallenge: 1, + codeChallengeMethod: 1, + }, + filter: { + code, } - }; - } + }, {}) - const expiresIn = 3600; // 1 hour - const refreshTokenExpiresIn = 86400 * 30; // 30 days + // 找不到记录 + if (!authCodeRecord) { + await context.commit(); + return { + statusCode: 400, + data: { error: "invalid_grant", error_description: "Invalid authorization code", success: false } + }; + } - // 如果过期就顺带刷新了(refresh token没过期, accessToken过期了) - if (existingToken && (existingToken.refreshExpiresAt as number > Date.now())) { - console.log("Existing token expired, refreshing it"); - 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 as string, + authCodeRecord.codeChallengeMethod as 'plain' | 'S256' + ); + + if (!isValid) { + await context.commit(); + return { + statusCode: 400, + data: { + error: "invalid_grant", + error_description: "Invalid code_verifier", + success: false + } + }; + } + } + + // 验证redirect_uri + if (authCodeRecord.redirectUri !== redirect_uri) { + await context.commit(); + return { + statusCode: 400, + data: { error: "invalid_grant", error_description: "Redirect URI mismatch", success: false } + }; + } + + // 验证过期 + if (authCodeRecord.expiresAt as number < 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 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 + + // 如果过期就顺带刷新了(refresh token没过期, accessToken过期了) + 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, + }, + 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", { + 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: { - lastUsedAt: Date.now(), - accessToken: newAccessToken, - refreshToken: newRefreshToken, - accessExpiresAt: Date.now() + expiresIn * 1000, - refreshExpiresAt: Date.now() + refreshTokenExpiresIn * 1000, + tokenId: tokenId, + usageState: 'granted', + }, + filter: { + codeId: authCodeRecord.id, } }, {}) - // // 创建记录 - // await context.operate("oauthUserAuthorization", { - // id: await generateNewIdAsync(), - // action: "update", - // data: { - // tokenId: existingToken.id, - // 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: newAccessToken, + access_token: genaccessToken, token_type: "Bearer", expires_in: expiresIn, - refresh_token: newRefreshToken, + refresh_token: refreshToken, refresh_expires_in: refreshTokenExpiresIn, success: true, } }; + } catch (err) { + console.error("Error in oauth token endpoint:", err); + await context.rollback(); + return { + statusCode: 500, + data: { error: "server_error", error_description: "Internal server error: " + (err instanceof Error ? err.message : String(err)), success: false } + }; } - - // 有一种情况是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", { - 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, - } - }; } } @@ -359,39 +371,48 @@ const oauthUserInfoEndpoint: Endpoint UserID | Promise const handlerMap = new Map(); 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); } @@ -38,7 +35,6 @@ Object.entries(defaulthandlers).forEach(([type, handler]) => { registerOauthUserinfoHandler(type as EntityDict['oauthProvider']['Schema']['type'], handler); }); - function validateToken(token: string | undefined): { token: string; error: string | null;