Add OAuth 2.0 authorization server to enable ChatGPT Web integration (#413)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
Co-authored-by: samanhappy <samanhappy@gmail.com>
This commit is contained in:
Copilot
2025-11-21 13:25:02 +08:00
committed by GitHub
parent 1869f283ba
commit 449e6ea4fd
34 changed files with 4930 additions and 103 deletions

View File

@@ -9,6 +9,8 @@ import { loadSettings } from '../config/index.js';
import config from '../config/index.js';
import { UserContextService } from './userContextService.js';
import { RequestContextService } from './requestContextService.js';
import { IUser } from '../types/index.js';
import { resolveOAuthUserFromToken } from '../utils/oauthBearer.js';
export const transports: { [sessionId: string]: { transport: Transport; group: string; needsInitialization?: boolean } } = {};
@@ -19,8 +21,14 @@ export const getGroup = (sessionId: string): string => {
return transports[sessionId]?.group || '';
};
// Helper function to validate bearer auth
const validateBearerAuth = (req: Request): boolean => {
type BearerAuthResult =
| { valid: true; user?: IUser }
| {
valid: false;
reason: 'missing' | 'invalid';
};
const validateBearerAuth = (req: Request): BearerAuthResult => {
const settings = loadSettings();
const routingConfig = settings.systemConfig?.routing || {
enableGlobalRoute: true,
@@ -32,29 +40,145 @@ const validateBearerAuth = (req: Request): boolean => {
if (routingConfig.enableBearerAuth) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return false;
return { valid: false, reason: 'missing' };
}
const token = authHeader.substring(7); // Remove "Bearer " prefix
return token === routingConfig.bearerAuthKey;
if (token.trim().length === 0) {
return { valid: false, reason: 'missing' };
}
if (token === routingConfig.bearerAuthKey) {
return { valid: true };
}
const oauthUser = resolveOAuthUserFromToken(token);
if (oauthUser) {
return { valid: true, user: oauthUser };
}
return { valid: false, reason: 'invalid' };
}
return true;
return { valid: true };
};
const attachUserContextFromBearer = (result: BearerAuthResult, res: Response): void => {
if (!result.valid || !result.user) {
return;
}
const userContextService = UserContextService.getInstance();
if (userContextService.hasUser()) {
return;
}
userContextService.setCurrentUser(result.user);
let cleanedUp = false;
const cleanup = () => {
if (cleanedUp) {
return;
}
cleanedUp = true;
userContextService.clearCurrentUser();
};
res.on('finish', cleanup);
res.on('close', cleanup);
};
const escapeHeaderValue = (value: string): string => {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
};
const buildResourceMetadataUrl = (req: Request): string | undefined => {
const forwardedProto = (req.headers['x-forwarded-proto'] as string | undefined)
?.split(',')[0]
?.trim();
const protocol = forwardedProto || req.protocol || 'http';
const forwardedHost = (req.headers['x-forwarded-host'] as string | undefined)
?.split(',')[0]
?.trim();
const host =
forwardedHost ||
(req.headers.host as string | undefined) ||
(req.headers[':authority'] as string | undefined);
if (!host) {
return undefined;
}
const origin = `${protocol}://${host}`;
const basePath = config.basePath || '';
if (!basePath || basePath === '/') {
return `${origin}/.well-known/oauth-protected-resource`;
}
const normalizedBasePath = `${basePath.startsWith('/') ? '' : '/'}${basePath}`.replace(
/\/+$/,
'',
);
return `${origin}/.well-known/oauth-protected-resource${normalizedBasePath}`;
};
const sendBearerAuthError = (req: Request, res: Response, reason: 'missing' | 'invalid'): void => {
const errorDescription =
reason === 'missing' ? 'No authorization provided' : 'Invalid bearer token';
const resourceMetadataUrl = buildResourceMetadataUrl(req);
const headerParts = [
'error="invalid_token"',
`error_description="${escapeHeaderValue(errorDescription)}"`,
];
if (resourceMetadataUrl) {
headerParts.push(`resource_metadata="${escapeHeaderValue(resourceMetadataUrl)}"`);
}
console.warn(
reason === 'missing'
? 'Bearer authentication required but no authorization header was provided'
: 'Bearer authentication failed due to invalid bearer token',
);
res.setHeader('WWW-Authenticate', `Bearer ${headerParts.join(', ')}`);
const responseBody: {
error: string;
error_description: string;
resource_metadata?: string;
} = {
error: 'invalid_token',
error_description: errorDescription,
};
if (resourceMetadataUrl) {
responseBody.resource_metadata = resourceMetadataUrl;
}
res.status(401).json(responseBody);
};
export const handleSseConnection = async (req: Request, res: Response): Promise<void> => {
// User context is now set by sseUserContextMiddleware
const userContextService = UserContextService.getInstance();
const currentUser = userContextService.getCurrentUser();
const username = currentUser?.username;
// Check bearer auth using filtered settings
if (!validateBearerAuth(req)) {
console.warn('Bearer authentication failed or not provided');
res.status(401).send('Bearer authentication required or invalid token');
const bearerAuthResult = validateBearerAuth(req);
if (!bearerAuthResult.valid) {
sendBearerAuthError(req, res, bearerAuthResult.reason);
return;
}
attachUserContextFromBearer(bearerAuthResult, res);
const currentUser = userContextService.getCurrentUser();
const username = currentUser?.username;
const settings = loadSettings();
const routingConfig = settings.systemConfig?.routing || {
enableGlobalRoute: true,
@@ -102,15 +226,19 @@ export const handleSseConnection = async (req: Request, res: Response): Promise<
export const handleSseMessage = async (req: Request, res: Response): Promise<void> => {
// User context is now set by sseUserContextMiddleware
const userContextService = UserContextService.getInstance();
const currentUser = userContextService.getCurrentUser();
const username = currentUser?.username;
// Check bearer auth using filtered settings
if (!validateBearerAuth(req)) {
res.status(401).send('Bearer authentication required or invalid token');
const bearerAuthResult = validateBearerAuth(req);
if (!bearerAuthResult.valid) {
sendBearerAuthError(req, res, bearerAuthResult.reason);
return;
}
attachUserContextFromBearer(bearerAuthResult, res);
const currentUser = userContextService.getCurrentUser();
const username = currentUser?.username;
const sessionId = req.query.sessionId as string;
// Validate sessionId
@@ -220,6 +348,16 @@ async function createNewSession(group: string, username?: string): Promise<Strea
export const handleMcpPostRequest = async (req: Request, res: Response): Promise<void> => {
// User context is now set by sseUserContextMiddleware
const userContextService = UserContextService.getInstance();
// Check bearer auth using filtered settings
const bearerAuthResult = validateBearerAuth(req);
if (!bearerAuthResult.valid) {
sendBearerAuthError(req, res, bearerAuthResult.reason);
return;
}
attachUserContextFromBearer(bearerAuthResult, res);
const currentUser = userContextService.getCurrentUser();
const username = currentUser?.username;
@@ -230,12 +368,6 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise
`Handling MCP post request for sessionId: ${sessionId} and group: ${group}${username ? ` for user: ${username}` : ''} with body: ${JSON.stringify(body)}`,
);
// Check bearer auth using filtered settings
if (!validateBearerAuth(req)) {
res.status(401).send('Bearer authentication required or invalid token');
return;
}
// Get filtered settings based on user context (after setting user context)
const settings = loadSettings();
const routingConfig = settings.systemConfig?.routing || {
@@ -443,17 +575,21 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise
export const handleMcpOtherRequest = async (req: Request, res: Response) => {
// User context is now set by sseUserContextMiddleware
const userContextService = UserContextService.getInstance();
// Check bearer auth using filtered settings
const bearerAuthResult = validateBearerAuth(req);
if (!bearerAuthResult.valid) {
sendBearerAuthError(req, res, bearerAuthResult.reason);
return;
}
attachUserContextFromBearer(bearerAuthResult, res);
const currentUser = userContextService.getCurrentUser();
const username = currentUser?.username;
console.log(`Handling MCP other request${username ? ` for user: ${username}` : ''}`);
// Check bearer auth using filtered settings
if (!validateBearerAuth(req)) {
res.status(401).send('Bearer authentication required or invalid token');
return;
}
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId) {
res.status(400).send('Invalid or missing session ID');