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 => { 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 => { 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 => { 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 => { 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); };