import { Request, Response } from 'express'; import { DyFM_Error, DyFM_Log } from '@futdevpro/fsm-dynamo'; import { DyNTS_AuthService } from '../../../_services/core/auth.service'; import { DyNTS_global_settings } from '../../../_collections/global-settings.const'; import { DyNTS_OAuth2_ControlService } from './oauth2.control-service'; /** * OAuth2 Authentication Service implementation * * This service handles OAuth2 specific authentication flows and token management * * @example * const authService = DyNTS_OAuth2_AuthService.getInstance(); * await authService.authenticate_token(req, res); */ export class DyNTS_OAuth2_AuthService extends DyNTS_AuthService { static getInstance(): DyNTS_OAuth2_AuthService { return DyNTS_OAuth2_AuthService.getSingletonInstance(); } private readonly controlService: DyNTS_OAuth2_ControlService = DyNTS_OAuth2_ControlService.getInstance(); readonly authenticate_token = async (req: Request, res: Response): Promise => { try { const token = this.getTokenFromRequest(req); // Validate token format if (!token?.startsWith('Bearer ')) { throw new DyFM_Error({ status: 401, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-AT1`, addECToUserMsg: true, message: 'Invalid token format', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, }); } const accessToken = token.substring(7); // Remove 'Bearer ' prefix // Validate token against stored tokens const tokenData = this.controlService.getAccessTokenData(accessToken); if (!tokenData || tokenData.expiresAt < Date.now()) { throw new DyFM_Error({ status: 401, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-AT2`, addECToUserMsg: true, message: 'Invalid or expired token', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, }); } // Set token in response header res.setHeader('authorization', `Bearer ${accessToken}`); } catch (error) { DyFM_Log.error('OAuth2 token authentication failed', error); throw new DyFM_Error({ status: 401, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-AT0`, addECToUserMsg: true, message: 'OAuth2 token authentication failed', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, error }); } }; readonly authenticate_tokenSelf = async (req: Request, res: Response): Promise => { try { const token = this.getTokenFromRequest(req); // Validate token format if (!token || !token.startsWith('Bearer ')) { throw new DyFM_Error({ status: 401, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-ATS1`, addECToUserMsg: true, message: 'Invalid token format', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, }); } const accessToken = token.substring(7); // Remove 'Bearer ' prefix // Validate token against stored tokens const tokenData = this.controlService.getAccessTokenData(accessToken); if (!tokenData || tokenData.expiresAt < Date.now()) { throw new DyFM_Error({ status: 401, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-ATS2`, addECToUserMsg: true, message: 'Invalid or expired token', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, }); } // For self-token validation, ensure the token is associated with the requesting user const issuer = this.getIssuerFromRequest(req); if (!issuer || issuer !== tokenData.clientId) { throw new DyFM_Error({ status: 403, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-ATS3`, addECToUserMsg: true, message: 'Token not associated with requesting user', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, }); } // Set token in response header res.setHeader('authorization', `Bearer ${accessToken}`); } catch (error) { DyFM_Log.error('OAuth2 self-token authentication failed', error); throw new DyFM_Error({ status: 401, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-ATS0`, addECToUserMsg: true, message: 'OAuth2 self-token authentication failed', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, error }); } }; readonly authenticate_tokenPerm_accUsageData = async (req: Request, res: Response): Promise => { try { const token = this.getTokenFromRequest(req); // Validate token format if (!token || !token.startsWith('Bearer ')) { throw new DyFM_Error({ status: 401, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-ATU1`, addECToUserMsg: true, message: 'Invalid token format', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, }); } const accessToken = token.substring(7); // Remove 'Bearer ' prefix // Validate token against stored tokens const tokenData = this.controlService.getAccessTokenData(accessToken); if (!tokenData || tokenData.expiresAt < Date.now()) { throw new DyFM_Error({ status: 401, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-ATU2`, addECToUserMsg: true, message: 'Invalid or expired token', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, }); } // Check if token has usage data permission if (!tokenData.scope.includes('usage_data')) { throw new DyFM_Error({ status: 403, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-ATU3`, addECToUserMsg: true, message: 'Token does not have usage data permission', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, }); } // Set token in response header res.setHeader('authorization', `Bearer ${accessToken}`); } catch (error) { DyFM_Log.error('OAuth2 usage data permission check failed', error); throw new DyFM_Error({ status: 401, errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OA2-ATU0`, addECToUserMsg: true, message: 'OAuth2 usage data permission check failed', userMessage: this.defaultErrorUserMsg, issuerService: this.serviceName, error }); } }; /** * Gets the issuer (user ID) from the OAuth2 token in the request * @param req Express Request object * @returns The issuer ID from the token */ getIssuerFromRequest(req: Request): string { try { const token = this.getTokenFromRequest(req); if (!token || !token.startsWith('Bearer ')) { return undefined; } const accessToken = token.substring(7); // Remove 'Bearer ' prefix const tokenData = this.controlService.getAccessTokenData(accessToken); return tokenData?.clientId; } catch { return undefined; } } /** * Gets the username from the OAuth2 token in the request * @param req Express Request object * @returns The username from the token */ getUsernameFromRequest(req: Request): string { try { const token = this.getTokenFromRequest(req); if (!token || !token.startsWith('Bearer ')) { return undefined; } const accessToken = token.substring(7); // Remove 'Bearer ' prefix const tokenData = this.controlService.getAccessTokenData(accessToken); // TODO: Implement user information retrieval from database/storage // For now, return the client ID as username return tokenData?.clientId; } catch { return undefined; } } }