diff --git a/libs/logger/src/interceptors/common-http.interceptor.spec.ts b/libs/logger/src/interceptors/common-http.interceptor.spec.ts index 80be662f..f04f0d52 100644 --- a/libs/logger/src/interceptors/common-http.interceptor.spec.ts +++ b/libs/logger/src/interceptors/common-http.interceptor.spec.ts @@ -1,13 +1,16 @@ import { createMock } from '@golevelup/nestjs-testing'; import { CallHandler, ExecutionContext } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { of } from 'rxjs'; import { HttpLoggingInterceptor } from './common-http.interceptor'; describe('HttpLoggingInterceptor', () => { let interceptor; + let configService; beforeEach(() => { - interceptor = new HttpLoggingInterceptor(); + configService = new ConfigService(); + interceptor = new HttpLoggingInterceptor(configService); }); it('should be defined', () => { diff --git a/libs/logger/src/interceptors/common-http.interceptor.ts b/libs/logger/src/interceptors/common-http.interceptor.ts index 2d0c3455..084fb45e 100644 --- a/libs/logger/src/interceptors/common-http.interceptor.ts +++ b/libs/logger/src/interceptors/common-http.interceptor.ts @@ -1,10 +1,16 @@ import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class HttpLoggingInterceptor implements NestInterceptor { readonly logger = new Logger(HttpLoggingInterceptor.name); + #instanceID: string; + + constructor(configService: ConfigService) { + this.#instanceID = configService.get('SERVER_INSTANCE_ID'); + } intercept(context: ExecutionContext, next: CallHandler): Observable { let req = context.switchToHttp().getRequest(); @@ -12,13 +18,13 @@ export class HttpLoggingInterceptor implements NestInterceptor { if (context['contextType'] !== 'graphql') { log = true; - this.logger.log(`START: ${context.getClass().name}.${context.getHandler().name}(): ${req.method} ${req.url}`); + this.logger.log(`START: ${context.getClass().name}.${context.getHandler().name}(): ${req.method} ${req.url} instanceID: ${this.#instanceID}`); } return next.handle().pipe( tap(() => { if (log) { - this.logger.log(`STOP: ${context.getClass().name}.${context.getHandler().name}(): ${req.method} ${req.url}`); + this.logger.log(`STOP: ${context.getClass().name}.${context.getHandler().name}(): ${req.method} ${req.url} instanceID: ${this.#instanceID}`); } }), ); diff --git a/libs/logger/src/logger.module.ts b/libs/logger/src/logger.module.ts index 83abcd26..e232f569 100644 --- a/libs/logger/src/logger.module.ts +++ b/libs/logger/src/logger.module.ts @@ -1,8 +1,16 @@ -import { Module } from '@nestjs/common'; +import { DynamicModule, Module } from '@nestjs/common'; import { OMSLogger } from './oms/oms.logger.service'; @Module({ - providers: [ OMSLogger ], - exports: [ OMSLogger ], + providers: [ + OMSLogger + ], + exports: [OMSLogger], }) -export class LoggerModule {} +export class LoggerModule { + static register(): DynamicModule { + return { + module: LoggerModule + }; + } +} diff --git a/libs/logger/src/oms/oms.logger.service.spec.ts b/libs/logger/src/oms/oms.logger.service.spec.ts index 6f451d17..7ec3538e 100644 --- a/libs/logger/src/oms/oms.logger.service.spec.ts +++ b/libs/logger/src/oms/oms.logger.service.spec.ts @@ -1,15 +1,18 @@ +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { OMSLogger } from './oms.logger.service'; describe('OMSLogger', () => { let service: OMSLogger; + let configService: ConfigService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [OMSLogger], + providers: [OMSLogger, ConfigService], }).compile(); service = await module.resolve(OMSLogger); + configService = await module.resolve(ConfigService); jest.spyOn(console, 'log').mockImplementation(() => 'test'); process.stdout.isTTY = false; @@ -52,13 +55,15 @@ describe('OMSLogger', () => { describe('OMSLogger - with mocked interactive console', () => { let service: OMSLogger; + let configService: ConfigService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [OMSLogger], + providers: [OMSLogger, ConfigService], }).compile(); service = await module.resolve(OMSLogger); + configService = await module.resolve(ConfigService); jest.spyOn(console, 'log').mockImplementation(() => 'test'); process.stdout.isTTY = true; diff --git a/libs/logger/src/oms/oms.logger.service.ts b/libs/logger/src/oms/oms.logger.service.ts index 1e66b066..49e21964 100644 --- a/libs/logger/src/oms/oms.logger.service.ts +++ b/libs/logger/src/oms/oms.logger.service.ts @@ -1,8 +1,15 @@ import { ConsoleLogger, Injectable, Scope } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { OMSLogLevel } from './OMSLog.interface'; @Injectable({ scope: Scope.TRANSIENT }) export class OMSLogger extends ConsoleLogger { + #instanceID: string; + constructor(private configService: ConfigService) { + super(); + this.#instanceID = configService.get('SERVER_INSTANCE_ID'); + } + private print(logLevel: OMSLogLevel, message: string, context?: string, stackTrace?: string) { let currentContext = context; if (typeof context === 'undefined') { @@ -15,6 +22,7 @@ export class OMSLogger extends ConsoleLogger { msg: message, logger: currentContext, stack_trace: stackTrace, + instanceID: this.#instanceID, }; console.log(JSON.stringify(logEntry)); @@ -41,4 +49,4 @@ export class OMSLogger extends ConsoleLogger { verbose(message: string, context?: string) { process.stdout.isTTY ? super.verbose.apply(this, arguments) : this.print(OMSLogLevel.VERBOSE, message, context); } -} +} \ No newline at end of file diff --git a/libs/oidc/src/filters/http-exception.filter.ts b/libs/oidc/src/filters/http-exception.filter.ts index 57abff05..da286bea 100644 --- a/libs/oidc/src/filters/http-exception.filter.ts +++ b/libs/oidc/src/filters/http-exception.filter.ts @@ -7,7 +7,7 @@ import { SSRPagesService } from '../services'; export class HttpExceptionFilter implements ExceptionFilter { readonly logger = new Logger(HttpExceptionFilter.name); - constructor(private ssrPagesService: SSRPagesService) {} + constructor(private ssrPagesService: SSRPagesService) { } catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); diff --git a/libs/oidc/src/oidc.module.spec.ts b/libs/oidc/src/oidc.module.spec.ts index 3336564d..1e212dd9 100644 --- a/libs/oidc/src/oidc.module.spec.ts +++ b/libs/oidc/src/oidc.module.spec.ts @@ -1,11 +1,11 @@ -import { TestingModule, Test } from '@nestjs/testing'; import { createMock } from '@golevelup/nestjs-testing'; +import { MiddlewareConsumer } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; import { Issuer } from 'openid-client'; +import { MOCK_ISSUER_INSTANCE, MOCK_OIDC_MODULE_OPTIONS } from './mocks'; import { OidcModule } from './oidc.module'; -import { MOCK_OIDC_MODULE_OPTIONS, MOCK_ISSUER_INSTANCE } from './mocks'; -import { MiddlewareConsumer } from '@nestjs/common'; -describe('OidcModule', () => { +describe(OidcModule.name, () => { describe('register sync', () => { let module: TestingModule; diff --git a/libs/oidc/src/oidc.module.ts b/libs/oidc/src/oidc.module.ts index 31aa7df0..ff7aa0e5 100644 --- a/libs/oidc/src/oidc.module.ts +++ b/libs/oidc/src/oidc.module.ts @@ -1,4 +1,5 @@ import { DynamicModule, Global, MiddlewareConsumer, Module, NestModule, Provider, RequestMethod } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { APP_FILTER, APP_GUARD } from '@nestjs/core'; import { JwtModule } from '@nestjs/jwt'; import { TenantSwitchController } from './controllers'; @@ -30,6 +31,7 @@ import { SessionSerializer } from './utils/session.serializer'; TokenGuard, GuestTokenGuard, OidcService, + ConfigService, SSRPagesService, { provide: APP_GUARD, @@ -66,7 +68,7 @@ export class OidcModule implements NestModule { { provide: OIDC_MODULE_OPTIONS, useValue: options, - }, + } ], }; } @@ -75,7 +77,7 @@ export class OidcModule implements NestModule { return { module: OidcModule, imports: options.imports, - providers: [...this.createAsyncProviders(options)], + providers: this.createAsyncProviders(options), }; } diff --git a/libs/oidc/src/services/oidc.service.spec.ts b/libs/oidc/src/services/oidc.service.spec.ts index ed1619cd..9440d1be 100644 --- a/libs/oidc/src/services/oidc.service.spec.ts +++ b/libs/oidc/src/services/oidc.service.spec.ts @@ -1,3 +1,4 @@ +import { ConfigService } from '@nestjs/config'; import axios from 'axios'; import * as handlebars from 'handlebars'; import { JWKS } from 'jose'; @@ -11,7 +12,7 @@ import { SSRPagesService } from './ssr-pages.service'; import passport = require('passport'); describe('OidcService', () => { - let service = new OidcService(MOCK_OIDC_MODULE_OPTIONS, new SSRPagesService()); + let service = new OidcService(MOCK_OIDC_MODULE_OPTIONS, new ConfigService(), new SSRPagesService()); let options: OidcModuleOptions = MOCK_OIDC_MODULE_OPTIONS; const idpKey = 'idpKey'; @@ -154,7 +155,7 @@ describe('OidcService', () => { it('should call passport authenticate for single tenant login', async () => { service.strategy = new OidcStrategy(service, idpKey); const spy = jest.spyOn(passport, 'authenticate').mockImplementation(() => { - return (req, res, next) => {}; + return (req, res, next) => { }; }); await service.login(req, res, next, params); expect(spy).toHaveBeenCalled(); @@ -167,7 +168,7 @@ describe('OidcService', () => { channelType: 'b2c', }; const spy = jest.spyOn(passport, 'authenticate').mockImplementation(() => { - return (req, res, next) => {}; + return (req, res, next) => { }; }); await service.login(req, res, next, params); expect(spy).toHaveBeenCalled(); @@ -180,7 +181,7 @@ describe('OidcService', () => { tenantId: 'tenant', }; const spy = jest.spyOn(passport, 'authenticate').mockImplementation(() => { - return (req, res, next) => {}; + return (req, res, next) => { }; }); await service.login(req, res, next, params); expect(spy).toHaveBeenCalled(); @@ -211,7 +212,7 @@ describe('OidcService', () => { const spy = jest.spyOn(passport, 'authenticate').mockImplementation((strategy, options, cb) => { cb(); - return (req, res, next) => {}; + return (req, res, next) => { }; }); await service.login(req, res, next, params); @@ -233,7 +234,7 @@ describe('OidcService', () => { const spy = jest.spyOn(passport, 'authenticate').mockImplementation((strategy, options, cb) => { const user = {}; cb(null, user, null); - return (req, res, next) => {}; + return (req, res, next) => { }; }); await service.login(req, res, next, params); @@ -254,7 +255,7 @@ describe('OidcService', () => { const spy = jest.spyOn(passport, 'authenticate').mockImplementation((strategy, options, cb) => { cb(null, {}, null); - return (req, res, next) => {}; + return (req, res, next) => { }; }); await service.login(req, res, next, params); @@ -279,7 +280,7 @@ describe('OidcService', () => { state: options.state, }; cb(null, {}, null); - return (req, res, next) => {}; + return (req, res, next) => { }; }); const spyRes = jest.spyOn(res, 'redirect'); @@ -332,7 +333,7 @@ describe('OidcService', () => { state: options.state, }; cb(null, {}, null); - return (req, res, next) => {}; + return (req, res, next) => { }; }); const spyRes = jest.spyOn(res, 'redirect'); @@ -364,7 +365,7 @@ describe('OidcService', () => { state: options.state, }; cb(null, {}, null); - return (req, res, next) => {}; + return (req, res, next) => { }; }); const spyRes = jest.spyOn(res, 'redirect'); diff --git a/libs/oidc/src/services/oidc.service.ts b/libs/oidc/src/services/oidc.service.ts index 9f1153a8..f39729cf 100644 --- a/libs/oidc/src/services/oidc.service.ts +++ b/libs/oidc/src/services/oidc.service.ts @@ -1,4 +1,5 @@ import { HttpStatus, Inject, Injectable, Logger, Next, OnModuleInit, Param, Req, Res } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import axios from 'axios'; import { Request, Response } from 'express'; import * as handlebars from 'handlebars'; @@ -13,19 +14,19 @@ import { loginPopupTemplate } from '../templates/login-popup.hbs'; import { SSRPagesService } from './ssr-pages.service'; import passport = require('passport'); -const logger = new Logger('OidcService'); - -declare module 'express-session' { - interface SessionData { - tenant: string; - channel: string; - } -} +// declare module 'express-session' { +// interface SessionData { +// tenant: string; +// channel: string; +// } +// } @Injectable() export class OidcService implements OnModuleInit { + readonly logger = new Logger(OidcService.name); isMultitenant: boolean = false; strategy: any; + #instanceID: string; idpInfos: { [tokenName: string]: { trustIssuer: Issuer; @@ -37,9 +38,11 @@ export class OidcService implements OnModuleInit { constructor( @Inject(OIDC_MODULE_OPTIONS) public options: OidcModuleOptions, + configService: ConfigService, private ssrPagesService: SSRPagesService, ) { this.isMultitenant = !!this.options.issuerOrigin; + this.#instanceID = configService.get('SERVER_INSTANCE_ID'); } async onModuleInit() { @@ -93,21 +96,21 @@ export class OidcService implements OnModuleInit { return strategy; } catch (err) { if (this.isMultitenant) { - const errorMsg = { - error: err.message, + const errorMsg = JSON.stringify({ + error: err?.message, debug: { origin: this.options.origin, tenantId, channelType, }, - }; - logger.error(errorMsg); + }); + this.logger.error(`err?.message`, err?.stack, errorMsg); throw new Error(); } const docUrl = 'https://github.com/finastra/finastra-nodejs-libs/blob/develop/libs/oidc/README.md'; const msg = `Error accessing the issuer/tokenStore. Check if the url is valid or increase the timeout in the defaultHttpOptions : ${docUrl}`; - logger.error(msg); - logger.log('Terminating application'); + this.logger.error(msg, err?.stack); + this.logger.log('Terminating application'); process.exit(1); } } @@ -125,6 +128,7 @@ export class OidcService implements OnModuleInit { } async login(@Req() req: Request, @Res() res: Response, @Next() next: Function, @Param() params) { + this.logger.log(`LOGIN url: ${req.url}, session: ${JSON.stringify(req.session)}, ip: ${req.ip}, method: ${req.method}, instanceID: ${this.#instanceID}`); try { const tenantId = params.tenantId || req.session['tenant']; const channel = this.options.channelType || params.channelType || req.session['channel']; @@ -164,6 +168,8 @@ export class OidcService implements OnModuleInit { JSON.stringify({ redirect_url: `${prefix}${redirect_url}`, loginpopup: loginpopup }), 'utf-8', ).toString('base64'); + this.logger.log(`PRE_AUTHENTICATE url: ${req.url}, session: ${JSON.stringify(req.session)}, ip: ${req.ip}, method: ${req.method}, instanceID: ${this.#instanceID}`); + passport.authenticate( Object.create(strategy), { @@ -173,10 +179,13 @@ export class OidcService implements OnModuleInit { }, (err, user, info) => { if (err || !user) { + this.logger.error(`AUTHENTICATE url: ${req.url}, session: ${JSON.stringify(req.session)}, ip: ${req.ip}, method: ${req.method}, error message: ${err?.message}, instanceID: ${this.#instanceID}`); return next(err || info); } + this.logger.log(`PRE_LOGIN_REQ url: ${req.url}, session: ${JSON.stringify(req.session)}, ip: ${req.ip}, method: ${req.method}, error message: ${err?.message}, instanceID: ${this.#instanceID}`); req.logIn(user, err => { if (err) { + this.logger.error(`LOGIN_REQ url: ${req.url}, session: ${JSON.stringify(req.session)}, ip: ${req.ip}, method: ${req.method}, error message: ${err?.message}, instanceID: ${this.#instanceID}`); return next(err); } this.updateSessionDuration(req); @@ -186,6 +195,7 @@ export class OidcService implements OnModuleInit { let url: string = state['redirect_url']; url = !url.startsWith('/') ? `/${url}` : url; const loginpopup = state['loginpopup']; + this.logger.log(`PRE_REDIRECT url: ${req.url}, session: ${JSON.stringify(req.session)}, ip: ${req.ip}, method: ${req.method}, error message: ${err?.message}, instanceID: ${this.#instanceID}`); if (loginpopup) { return res.send(` `); } else { + this.logger.log(`REDIRECT url: ${req.url}, session: ${JSON.stringify(req.session)}, ip: ${req.ip}, method: ${req.method}, instanceID: ${this.#instanceID}`); return res.redirect(url); } }); @@ -200,6 +211,7 @@ export class OidcService implements OnModuleInit { )(req, res, next); } } catch (err) { + this.logger.error(`CATCH url: ${req.url}, session: ${JSON.stringify(req.session)}, ip: ${req.ip}, method: ${req.method}, message: ${err.message}, instanceID: ${this.#instanceID}`); res.status(HttpStatus.NOT_FOUND).send(); } } @@ -215,8 +227,7 @@ export class OidcService implements OnModuleInit { this.idpInfos[this.getIdpInfosKey(tenantId, channelType)].trustIssuer.metadata.end_session_endpoint; if (end_session_endpoint) { res.redirect( - `${end_session_endpoint}?post_logout_redirect_uri=${ - this.options.redirectUriLogout ? this.options.redirectUriLogout : this.options.origin + `${end_session_endpoint}?post_logout_redirect_uri=${this.options.redirectUriLogout ? this.options.redirectUriLogout : this.options.origin }&client_id=${this.options.clientMetadata.client_id}${id_token ? '&id_token_hint=' + id_token : ''}`, ); } else { diff --git a/libs/oidc/src/strategies/oidc.strategy.spec.ts b/libs/oidc/src/strategies/oidc.strategy.spec.ts index 20563974..c373fac0 100644 --- a/libs/oidc/src/strategies/oidc.strategy.spec.ts +++ b/libs/oidc/src/strategies/oidc.strategy.spec.ts @@ -1,4 +1,5 @@ import { createMock } from '@golevelup/nestjs-testing'; +import { ConfigService } from '@nestjs/config'; import { JWKS } from 'jose'; import { TokenSet } from 'openid-client'; import { MOCK_CLIENT_INSTANCE, MOCK_OIDC_MODULE_OPTIONS, MOCK_TRUST_ISSUER } from '../mocks'; @@ -12,7 +13,7 @@ describe('OidcStrategy', () => { let mockTokenset: TokenSet; beforeEach(() => { - const mockOidcService = new OidcService(MOCK_OIDC_MODULE_OPTIONS, new SSRPagesService()); + const mockOidcService = new OidcService(MOCK_OIDC_MODULE_OPTIONS, new ConfigService(), new SSRPagesService()); const idpKey = 'idpKey'; mockOidcService.idpInfos[idpKey] = { client: MOCK_CLIENT_INSTANCE, diff --git a/libs/oidc/src/utils/user-info.spec.ts b/libs/oidc/src/utils/user-info.spec.ts index 93b467d7..93204cc8 100644 --- a/libs/oidc/src/utils/user-info.spec.ts +++ b/libs/oidc/src/utils/user-info.spec.ts @@ -1,3 +1,4 @@ +import { ConfigService } from '@nestjs/config'; import { JWKS } from 'jose'; import { UserInfoMethod } from '../interfaces'; import { MOCK_CLIENT_INSTANCE, MOCK_OIDC_MODULE_OPTIONS, MOCK_TRUST_ISSUER } from '../mocks'; @@ -15,7 +16,7 @@ describe('OidcStrategy', () => { let service; beforeEach(() => { - service = new OidcService(MOCK_OIDC_MODULE_OPTIONS, new SSRPagesService()); + service = new OidcService(MOCK_OIDC_MODULE_OPTIONS, new ConfigService(), new SSRPagesService()); service.idpInfos[idpKey] = { client: MOCK_CLIENT_INSTANCE, tokenStore: new JWKS.KeyStore([]), diff --git a/src/filters/http-exception-global.filter.ts b/src/filters/http-exception-global.filter.ts deleted file mode 100644 index b082d7a9..00000000 --- a/src/filters/http-exception-global.filter.ts +++ /dev/null @@ -1,27 +0,0 @@ - -import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from '@nestjs/common'; -import { Request, Response } from 'express'; - -@Catch(HttpException) -export class HttpExceptionGlobalFilter implements ExceptionFilter { - readonly logger = new Logger(HttpExceptionGlobalFilter.name); - - catch(exception: HttpException, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); - const status = exception.getStatus(); - - const { body, headers, method, params, query, url, user } = request; - - this.logger.error({ request: { body, headers, method, params, query, url, user }, exception }); - - response - .status(status) - .json({ - statusCode: status, - timestamp: new Date().toISOString(), - path: request.url - }); - } -} diff --git a/src/main.ts b/src/main.ts index f0f48c84..3e715896 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,6 @@ import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { readFileSync } from 'fs'; import { AppModule } from './app.module'; -import { HttpExceptionGlobalFilter } from './filters/http-exception-global.filter'; async function bootstrap() { const logger = new OMSLogger(); @@ -23,7 +22,6 @@ async function bootstrap() { }); app.useLogger(logger); app.useGlobalInterceptors(new HttpLoggingInterceptor()); - app.useGlobalFilters(new HttpExceptionGlobalFilter()); app.useGlobalGuards(app.get(TokenGuard));