oak-general-business/es/endpoints/oauth.js

588 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
import { randomUUID } from "crypto";
import { composeFileUrlBackend } from "../utils/cos/index.backend";
import assert from "assert";
import { checkOauthTokenAvaliable } from "../utils/oauth";
import { createHash } from "crypto";
// PKCE 验证函数
async function verifyPKCE(verifier, challenge, method) {
if (method === 'plain') {
return verifier === challenge;
}
else if (method === 'S256') {
const hash = createHash('sha256').update(verifier).digest();
const computed = hash.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return computed === challenge;
}
return false;
}
const oauthTokenEndpoint = {
name: "获取OAuth Token",
params: [],
method: 'post',
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const context = await contextBuilder();
try {
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 }
};
}
// 如果userIdcode里面的appId都一样则说明是这个用户在这个应用下第二次授权的直接返回
const [existingToken] = await context.select("oauthToken", {
data: {
id: 1,
accessToken: 1,
refreshToken: 1,
accessExpiresAt: 1,
refreshExpiresAt: 1,
},
filter: {
userId: authCodeRecord.userId,
code: {
oauthAppId: app.id,
},
revokedAt: {
$exists: false,
}
}
}, {});
// 如果存在且未过期,直接返回
if (existingToken && (existingToken.accessExpiresAt > 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: {
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 = {
name: "获取OAuth用户信息",
params: [],
method: 'get',
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const context = await contextBuilder();
const token = header.authorization; // Bearer token
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: 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
}
};
}
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 }
};
}
}
};
const refreshTokenEndpoint = {
name: "刷新OAuth令牌",
params: [],
method: 'post',
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const { refresh_token, grant_type } = body;
const { authorization } = header;
// 暂时只支持 refresh_token 模式
if (grant_type !== "refresh_token") {
return {
statusCode: 400,
data: { error: "unsupported_grant_type", error_description: "Only refresh_token grant type is supported", success: false }
};
}
// 根据 RFC 6749 规范,请求参数在 Body 中以表单格式application/x-www-form-urlencoded提交。
// authorization header 中包含 client_id 和 client_secret 的 Base64 编码
const decodedAuth = Buffer.from((authorization || '').split(' ')[1] || '', 'base64').toString('utf-8');
const [client_id, client_secret] = decodedAuth.split(':');
if (!client_id || !client_secret) {
return {
statusCode: 401,
data: { error: "invalid_client", error_description: "Missing client credentials", success: false }
};
}
if (!refresh_token) {
return {
statusCode: 400,
data: { error: "invalid_request", error_description: "Missing refresh_token", success: false }
};
}
const context = await contextBuilder();
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 }
};
}
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,
}
}
}, {});
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,
}
}, {});
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,
}
};
}
catch (err) {
console.error("Error in refresh 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 oauthRevocationEndpoint = {
name: "撤销OAuth令牌",
params: [],
method: 'post',
type: "free",
fn: async (contextBuilder, params, header, req, body) => {
const { token, token_type_hint } = body;
const { authorization } = header;
// 1. 验证请求参数
if (!token) {
return {
statusCode: 400,
data: { error: "invalid_request", error_description: "Missing token parameter", success: false }
};
}
// 2. 客户端认证(使用 Basic Auth
const decodedAuth = Buffer.from((authorization || '').split(' ')[1] || '', 'base64').toString('utf-8');
const [client_id, client_secret] = decodedAuth.split(':');
if (!client_id || !client_secret) {
return {
statusCode: 401,
data: { error: "invalid_client", error_description: "Missing client credentials", success: false }
};
}
const context = await contextBuilder();
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: 200,
data: {}
};
}
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 }
};
}
}
};
const endpoints = {
'oauth/access_token': oauthTokenEndpoint,
'oauth/userinfo': oauthUserInfoEndpoint,
'oauth/token': refreshTokenEndpoint,
'oauth/revoke': oauthRevocationEndpoint,
};
export default endpoints;