Files
mcphub/src/services/oauthServerService.ts
2025-11-21 13:25:02 +08:00

436 lines
11 KiB
TypeScript

import OAuth2Server from '@node-oauth/oauth2-server';
import { Request as ExpressRequest, Response as ExpressResponse } from 'express';
import { loadSettings } from '../config/index.js';
import { findUserByUsername, verifyPassword } from '../models/User.js';
import {
findOAuthClientById,
saveAuthorizationCode,
getAuthorizationCode,
revokeAuthorizationCode,
saveToken,
getToken,
revokeToken,
} from '../models/OAuth.js';
import crypto from 'crypto';
const { Request, Response } = OAuth2Server;
// OAuth2Server model implementation
const oauthModel: OAuth2Server.AuthorizationCodeModel & OAuth2Server.RefreshTokenModel = {
/**
* Get client by client ID
*/
getClient: async (clientId: string, clientSecret?: string) => {
const client = findOAuthClientById(clientId);
if (!client) {
return false;
}
// If client secret is provided, verify it
if (clientSecret && client.clientSecret) {
if (client.clientSecret !== clientSecret) {
return false;
}
}
return {
id: client.clientId,
clientId: client.clientId,
clientSecret: client.clientSecret,
redirectUris: client.redirectUris,
grants: client.grants,
};
},
/**
* Save authorization code
*/
saveAuthorizationCode: async (
code: OAuth2Server.AuthorizationCode,
client: OAuth2Server.Client,
user: OAuth2Server.User,
) => {
const settings = loadSettings();
const oauthConfig = settings.systemConfig?.oauthServer;
const lifetime = oauthConfig?.authorizationCodeLifetime || 300;
const scopeString = Array.isArray(code.scope) ? code.scope.join(' ') : code.scope;
const authCode = saveAuthorizationCode(
{
redirectUri: code.redirectUri,
scope: scopeString,
clientId: client.id,
username: user.username,
codeChallenge: code.codeChallenge,
codeChallengeMethod: code.codeChallengeMethod,
},
lifetime,
);
return {
authorizationCode: authCode,
expiresAt: new Date(Date.now() + lifetime * 1000),
redirectUri: code.redirectUri,
scope: code.scope,
client,
user: {
username: user.username,
},
codeChallenge: code.codeChallenge,
codeChallengeMethod: code.codeChallengeMethod,
};
},
/**
* Get authorization code
*/
getAuthorizationCode: async (authorizationCode: string) => {
const code = getAuthorizationCode(authorizationCode);
if (!code) {
return false;
}
const client = findOAuthClientById(code.clientId);
if (!client) {
return false;
}
const scopeArray = code.scope ? code.scope.split(' ') : undefined;
return {
authorizationCode: code.code,
expiresAt: code.expiresAt,
redirectUri: code.redirectUri,
scope: scopeArray,
client: {
id: client.clientId,
clientId: client.clientId,
clientSecret: client.clientSecret,
redirectUris: client.redirectUris,
grants: client.grants,
},
user: {
username: code.username,
},
codeChallenge: code.codeChallenge,
codeChallengeMethod: code.codeChallengeMethod,
};
},
/**
* Revoke authorization code
*/
revokeAuthorizationCode: async (code: OAuth2Server.AuthorizationCode) => {
revokeAuthorizationCode(code.authorizationCode);
return true;
},
/**
* Save access token and refresh token
*/
saveToken: async (
token: OAuth2Server.Token,
client: OAuth2Server.Client,
user: OAuth2Server.User,
) => {
const settings = loadSettings();
const oauthConfig = settings.systemConfig?.oauthServer;
const accessTokenLifetime = oauthConfig?.accessTokenLifetime || 3600;
const refreshTokenLifetime = oauthConfig?.refreshTokenLifetime || 1209600;
const scopeString = Array.isArray(token.scope) ? token.scope.join(' ') : token.scope;
const savedToken = saveToken(
{
scope: scopeString,
clientId: client.id,
username: user.username,
},
accessTokenLifetime,
refreshTokenLifetime,
);
const scopeArray = savedToken.scope ? savedToken.scope.split(' ') : undefined;
return {
accessToken: savedToken.accessToken,
accessTokenExpiresAt: savedToken.accessTokenExpiresAt,
refreshToken: savedToken.refreshToken,
refreshTokenExpiresAt: savedToken.refreshTokenExpiresAt,
scope: scopeArray,
client,
user: {
username: user.username,
},
};
},
/**
* Get access token
*/
getAccessToken: async (accessToken: string) => {
const token = getToken(accessToken);
if (!token) {
return false;
}
const client = findOAuthClientById(token.clientId);
if (!client) {
return false;
}
const scopeArray = token.scope ? token.scope.split(' ') : undefined;
return {
accessToken: token.accessToken,
accessTokenExpiresAt: token.accessTokenExpiresAt,
scope: scopeArray,
client: {
id: client.clientId,
clientId: client.clientId,
clientSecret: client.clientSecret,
redirectUris: client.redirectUris,
grants: client.grants,
},
user: {
username: token.username,
},
};
},
/**
* Get refresh token
*/
getRefreshToken: async (refreshToken: string) => {
const token = getToken(refreshToken);
if (!token || token.refreshToken !== refreshToken) {
return false;
}
const client = findOAuthClientById(token.clientId);
if (!client) {
return false;
}
const scopeArray = token.scope ? token.scope.split(' ') : undefined;
return {
refreshToken: token.refreshToken!,
refreshTokenExpiresAt: token.refreshTokenExpiresAt!,
scope: scopeArray,
client: {
id: client.clientId,
clientId: client.clientId,
clientSecret: client.clientSecret,
redirectUris: client.redirectUris,
grants: client.grants,
},
user: {
username: token.username,
},
};
},
/**
* Revoke token
*/
revokeToken: async (token: OAuth2Server.Token | OAuth2Server.RefreshToken) => {
const refreshToken = 'refreshToken' in token ? token.refreshToken : undefined;
if (refreshToken) {
revokeToken(refreshToken);
}
return true;
},
/**
* Verify scope
*/
verifyScope: async (token: OAuth2Server.Token, scope: string | string[]) => {
if (!token.scope) {
return false;
}
const requestedScopes = Array.isArray(scope) ? scope : scope.split(' ');
const tokenScopes = Array.isArray(token.scope) ? token.scope : (token.scope as string).split(' ');
return requestedScopes.every((s) => tokenScopes.includes(s));
},
/**
* Validate scope
*/
validateScope: async (user: OAuth2Server.User, client: OAuth2Server.Client, scope?: string[]) => {
const settings = loadSettings();
const oauthConfig = settings.systemConfig?.oauthServer;
const allowedScopes = oauthConfig?.allowedScopes || ['read', 'write'];
if (!scope || scope.length === 0) {
return allowedScopes;
}
const validScopes = scope.filter((s) => allowedScopes.includes(s));
return validScopes.length > 0 ? validScopes : false;
},
};
// Create OAuth2 server instance
let oauth: OAuth2Server | null = null;
/**
* Initialize OAuth server
*/
export const initOAuthServer = (): void => {
const settings = loadSettings();
const oauthConfig = settings.systemConfig?.oauthServer;
const requireState = oauthConfig?.requireState === true;
if (!oauthConfig || !oauthConfig.enabled) {
console.log('OAuth authorization server is disabled or not configured');
return;
}
try {
oauth = new OAuth2Server({
model: oauthModel,
accessTokenLifetime: oauthConfig.accessTokenLifetime || 3600,
refreshTokenLifetime: oauthConfig.refreshTokenLifetime || 1209600,
authorizationCodeLifetime: oauthConfig.authorizationCodeLifetime || 300,
allowEmptyState: !requireState,
allowBearerTokensInQueryString: false,
// When requireClientSecret is false, allow PKCE without client secret
requireClientAuthentication: oauthConfig.requireClientSecret
? { authorization_code: true, refresh_token: true }
: { authorization_code: false, refresh_token: false },
});
console.log('OAuth authorization server initialized successfully');
} catch (error) {
console.error('Failed to initialize OAuth authorization server:', error);
oauth = null;
}
};
/**
* Get OAuth server instance
*/
export const getOAuthServer = (): OAuth2Server | null => {
return oauth;
};
/**
* Check if OAuth server is enabled
*/
export const isOAuthServerEnabled = (): boolean => {
return oauth !== null;
};
/**
* Authenticate user for OAuth authorization
*/
export const authenticateUser = async (
username: string,
password: string,
): Promise<OAuth2Server.User | null> => {
const user = findUserByUsername(username);
if (!user) {
return null;
}
const isValid = await verifyPassword(password, user.password);
if (!isValid) {
return null;
}
return {
username: user.username,
isAdmin: user.isAdmin,
};
};
/**
* Generate PKCE code verifier
*/
export const generateCodeVerifier = (): string => {
return crypto.randomBytes(32).toString('base64url');
};
/**
* Generate PKCE code challenge from verifier
*/
export const generateCodeChallenge = (verifier: string): string => {
return crypto.createHash('sha256').update(verifier).digest('base64url');
};
/**
* Verify PKCE code challenge
*/
export const verifyCodeChallenge = (
verifier: string,
challenge: string,
method: string = 'S256',
): boolean => {
if (method === 'plain') {
return verifier === challenge;
}
if (method === 'S256') {
const computed = generateCodeChallenge(verifier);
return computed === challenge;
}
return false;
};
/**
* Handle OAuth authorize request
*/
export const handleAuthorizeRequest = async (
req: ExpressRequest,
res: ExpressResponse,
): Promise<OAuth2Server.AuthorizationCode> => {
if (!oauth) {
throw new Error('OAuth server not initialized');
}
const request = new Request(req);
const response = new Response(res);
return await oauth.authorize(request, response);
};
/**
* Handle OAuth token request
*/
export const handleTokenRequest = async (
req: ExpressRequest,
res: ExpressResponse,
): Promise<OAuth2Server.Token> => {
if (!oauth) {
throw new Error('OAuth server not initialized');
}
const request = new Request(req);
const response = new Response(res);
return await oauth.token(request, response);
};
/**
* Handle OAuth authenticate request (validate access token)
*/
export const handleAuthenticateRequest = async (
req: ExpressRequest,
res: ExpressResponse,
): Promise<OAuth2Server.Token> => {
if (!oauth) {
throw new Error('OAuth server not initialized');
}
const request = new Request(req);
const response = new Response(res);
return await oauth.authenticate(request, response);
};