From cb5ba591e03b015cb449d0cd0c0d82fac2c369d2 Mon Sep 17 00:00:00 2001 From: Timon Masberg Date: Fri, 11 Aug 2023 12:49:07 +0200 Subject: [PATCH 1/7] fix: env not loading due to changes introduced in #332 --- .gitignore | 2 +- CONTRIBUTING.md | 2 +- apps/api/.env.template | 8 ++++++++ apps/api/src/.env.template | 4 ---- apps/api/src/app/app.module.ts | 1 - 5 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 apps/api/.env.template delete mode 100644 apps/api/src/.env.template diff --git a/.gitignore b/.gitignore index 7c5ed58a..9f006d21 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,6 @@ Thumbs.db playwright/.auth # Environments -apps/api/src/.env +apps/api/.env apps/spa-e2e/.env apps/spa/src/environments/environment.prod.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd942c16..5f02c512 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,7 +84,7 @@ information about the folder and code structure please also read the [architecture documentation](docs/architecture.md). Before you can run the API application, you need to create a .env file from the -[.env.example](apps/api/src/.env.template) file. There you have to specify the +[.env.example](apps/api/.env.template) file. There you have to specify the MongoDB connection URI and more configurations. You can ignore some values as they are only used in production (they have a comment to clarify this). If you want to test the API directly without the SPA, you can use one of the diff --git a/apps/api/.env.template b/apps/api/.env.template new file mode 100644 index 00000000..12623de3 --- /dev/null +++ b/apps/api/.env.template @@ -0,0 +1,8 @@ +MONGODB_URI=$MONGODB_URI +ENVIRONMENT_NAME=$ENVIRONMENT_NAME# Prod +RELEASE_VERSION=$RELEASE_VERSION# Prod +SENTRY_KEY=$SENTRY_KEY# Prod +AADB2C_TENANT_NAME=$AADB2C_TENANT_NAME# Prod +AADB2C_SIGN_IN_POLICY=$AADB2C_SIGN_IN_POLICY# Prod +AADB2C_CLIENT_ID=$AADB2C_CLIENT_ID# Prod +AADB2C_ISSUER=$AADB2C_ISSUER# Prod diff --git a/apps/api/src/.env.template b/apps/api/src/.env.template deleted file mode 100644 index 09149cc2..00000000 --- a/apps/api/src/.env.template +++ /dev/null @@ -1,4 +0,0 @@ -MONGODB_URI=$MONGODB_URI -ENVIRONMENT_NAME=$ENVIRONMENT_NAME# Prod -RELEASE_VERSION=$RELEASE_VERSION# Prod -SENTRY_KEY=$SENTRY_KEY# Prod diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 2b8a5053..81e908da 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -33,7 +33,6 @@ const UTILITY_MODULES = [ ConfigModule.forRoot({ isGlobal: true, cache: true, - envFilePath: path.resolve(__dirname, '.env'), }), GraphQLModule.forRootAsync({ imports: [ConfigModule], From a83e5cdf92753f22130d1aa32be8d9224d079cc9 Mon Sep 17 00:00:00 2001 From: Timon Masberg Date: Fri, 11 Aug 2023 12:50:25 +0200 Subject: [PATCH 2/7] feat(api): add auth strategy for aadb2c jwt verification --- .../actions/build-and-deploy-api/action.yml | 16 +++ .github/workflows/next-deployment.yml | 4 + apps/api/src/app/app.module.ts | 2 +- .../verify-aadb2c-jwt.strategy.spec.ts | 136 ++++++++++++++++++ .../verify-aadb2c-jwt.strategy.ts | 82 +++++++++++ .../verify-auth-user.strategy.ts | 10 ++ .../verify-dev-bearer.strategy.spec.ts} | 21 ++- .../verify-dev-bearer.strategy.ts} | 16 +-- libs/api/auth/src/lib/auth.module.ts | 48 +++++-- .../lib/interceptors/auth.interceptor.spec.ts | 36 ++--- .../src/lib/interceptors/auth.interceptor.ts | 18 ++- libs/api/auth/tsconfig.json | 3 +- package-lock.json | 119 +++++++++++++++ package.json | 2 + 14 files changed, 456 insertions(+), 57 deletions(-) create mode 100644 libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.spec.ts create mode 100644 libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.ts create mode 100644 libs/api/auth/src/lib/auth-strategies/verify-auth-user.strategy.ts rename libs/api/auth/src/lib/{auth-user-extractor-strategies/extract-user-from-ms-principle-header.spec.ts => auth-strategies/verify-dev-bearer.strategy.spec.ts} (61%) rename libs/api/auth/src/lib/{auth-user-extractor-strategies/auth-user-extractor.strategy.ts => auth-strategies/verify-dev-bearer.strategy.ts} (64%) diff --git a/.github/actions/build-and-deploy-api/action.yml b/.github/actions/build-and-deploy-api/action.yml index 4ecc942c..d3807769 100644 --- a/.github/actions/build-and-deploy-api/action.yml +++ b/.github/actions/build-and-deploy-api/action.yml @@ -26,6 +26,18 @@ inputs: containerRegistryPassword: required: true description: "Container registry password" + aadb2cTenantName: + required: false + description: "Azure AD B2C Tenant Name" + aadb2cClientId: + required: false + description: "Azure AD B2C Client ID" + aadb2cIssuer: + required: false + description: "Azure AD B2C Token Issuer URL" + aadb2cPolicy: + required: false + description: "Azure AD B2C Sign In Policy" outputs: url: description: "API URL" @@ -68,6 +80,10 @@ runs: ENVIRONMENT_NAME: ${{ inputs.slot }} RELEASE_VERSION: ${{ inputs.releaseVersion }} SENTRY_KEY: ${{ inputs.sentryKey }} + AADB2C_TENANT_NAME: ${{ inputs.aadb2cTenantName }} + AADB2C_CLIENT_ID: ${{ inputs.aadb2cClientId }} + AADB2C_SIGN_IN_POLICY: ${{ inputs.aadb2cPolicy }} + AADB2C_ISSUER: ${{ inputs.aadb2cIssuer }} shell: bash - name: Deploy API id: wa-deployment diff --git a/.github/workflows/next-deployment.yml b/.github/workflows/next-deployment.yml index 8c87e92a..4bdb71df 100644 --- a/.github/workflows/next-deployment.yml +++ b/.github/workflows/next-deployment.yml @@ -41,6 +41,10 @@ jobs: containerRegistryUrl: ghcr.io containerRegistryUsername: ${{ github.actor }} containerRegistryPassword: ${{ secrets.GITHUB_TOKEN }} + aadb2cTenantName: ${{ secrets.AADB2C_TENANT }} + aadb2cClientId: ${{ secrets.DEV_AADB2C_CLIENT_ID }} + aadb2cIssuer: ${{ secrets.DEV_AADB2C_ISSUER }} + aadb2cPolicy: ${{ secrets.AADB2C_SIGN_IN_POLICY }} - name: Apply Database Migrations run: ./tools/db/kordis-db.sh apply-pending-migrations env: diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 81e908da..016fa87d 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -22,7 +22,7 @@ import { GraphqlSubscriptionsController } from './controllers/graphql-subscripti const FEATURE_MODULES = [OrganizationModule]; const UTILITY_MODULES = [ SharedKernel, - AuthModule, + AuthModule.forRoot('dev'), // todo: this needs to be changed to aadb2c when we move to onpremise ...(process.env.NODE_ENV === 'production' && !process.env.GITHUB_ACTIONS ? [SentryObservabilityModule] : [DevObservabilityModule]), diff --git a/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.spec.ts b/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.spec.ts new file mode 100644 index 00000000..807d4251 --- /dev/null +++ b/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.spec.ts @@ -0,0 +1,136 @@ +import { createMock } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import jwt from 'jsonwebtoken'; + +import { KordisRequest } from '@kordis/api/shared'; + +import { VerifyAADB2CJWTStrategy } from './verify-aadb2c-jwt.strategy'; + +jest.mock('jwks-rsa', () => () => { + return { + getKeys: jest.fn(), + getSigningKey: jest.fn().mockResolvedValue({ + kid: 'kid', + alg: 'alg', + getPublicKey: jest.fn().mockReturnValue('publicKey'), + rsaPublicKey: 'publicKey', + }), + getSigningKeys: jest.fn(), + }; +}); + +describe('VerifyAADB2CJWTStrategy', () => { + let verifyAADB2CJWTStrategy: VerifyAADB2CJWTStrategy; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ConfigService, + useValue: createMock({ + getOrThrow: jest + .fn() + .mockReturnValue('tenant') + .mockReturnValue('policy') + .mockReturnValue('clientId') + .mockReturnValue('issuer'), + }), + }, + VerifyAADB2CJWTStrategy, + ], + }).compile(); + + verifyAADB2CJWTStrategy = module.get( + VerifyAADB2CJWTStrategy, + ); + }); + + it('should verify and return user on valid JWT', async () => { + const req = createMock>({ + headers: { + authorization: 'Bearer 123', + }, + }); + + jest.spyOn(jwt, 'decode').mockImplementationOnce( + () => + ({ + header: { kid: 'mockKid' }, + payload: { + sub: 'id', + given_name: 'foo', + family_name: 'bar', + emails: ['foo@bar.de'], + organization: 'testorg', + }, + } as any), + ); + + const verifySpy = jest.spyOn(jwt, 'verify').mockReturnValueOnce(undefined); + await expect( + verifyAADB2CJWTStrategy.verifyUserFromRequest(req), + ).resolves.toEqual({ + id: 'id', + email: 'foo@bar.de', + firstName: 'foo', + lastName: 'bar', + organization: 'testorg', + }); + + expect(verifySpy).toHaveBeenCalledWith( + '123', + 'publicKey', + expect.anything(), + ); + }); + + it('should return null on empty authorization header', async () => { + const req = createMock>({ + headers: {}, + }); + + await expect( + verifyAADB2CJWTStrategy.verifyUserFromRequest(req), + ).resolves.toBeNull(); + }); + + it('should return null on invalid JWT', async () => { + const req = createMock>({ + headers: { + authorization: 'Bearer 123', + }, + }); + + jest.spyOn(jwt, 'decode').mockImplementationOnce( + () => + ({ + header: { kid: 'mockKid' }, + } as any), + ); + + jest.spyOn(jwt, 'verify').mockImplementationOnce(() => { + throw new Error('Invalid JWT'); + }); + + await expect( + verifyAADB2CJWTStrategy.verifyUserFromRequest(req), + ).resolves.toBeNull(); + }); + + it('should return null on jwt decode fail', async () => { + const req = createMock>({ + headers: { + authorization: 'Bearer 123', + }, + }); + + jest.spyOn(jwt, 'decode').mockImplementationOnce(() => null); + + await expect( + verifyAADB2CJWTStrategy.verifyUserFromRequest(req), + ).resolves.toBeNull(); + }); +}); diff --git a/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.ts b/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.ts new file mode 100644 index 00000000..ca6b75dd --- /dev/null +++ b/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; +import * as jwt from 'jsonwebtoken'; +import jwksClient from 'jwks-rsa'; + +import { AuthUser } from '@kordis/shared/auth'; + +import { VerifyAuthUserStrategy } from './verify-auth-user.strategy'; + +declare module 'jsonwebtoken' { + export interface JwtPayload { + sub?: string; + oid: string; + emails: string[]; + given_name: string; + family_name: string; + organization: string; + } +} + +@Injectable() +export class VerifyAADB2CJWTStrategy extends VerifyAuthUserStrategy { + private readonly client: jwksClient.JwksClient; + private readonly verifyOptions: jwt.VerifyOptions; + + constructor(config: ConfigService) { + super(); + + const tenant = config.getOrThrow('AADB2C_TENANT_NAME'); + const signInPolicy = config.getOrThrow('AADB2C_SIGN_IN_POLICY'); + const clientId = config.getOrThrow('AADB2C_CLIENT_ID'); + const issuer = config.getOrThrow('AADB2C_ISSUER'); + + this.verifyOptions = { + algorithms: ['RS256'], + audience: clientId, + issuer, + }; + this.client = jwksClient({ + jwksUri: `https://${tenant}.b2clogin.com/${tenant}.onmicrosoft.com/${signInPolicy}/discovery/v2.0/keys`, + }); + } + + async verifyUserFromRequest(req: Request): Promise { + const authHeaderValue = req.headers['authorization']; + + if (!authHeaderValue) { + return null; + } + const bearerToken = authHeaderValue.split(' ')[1]; + + const decodedToken = jwt.decode(bearerToken, { + complete: true, + }); + if (!decodedToken) { + return null; + } + + const key = await this.client.getSigningKey(decodedToken.header.kid); + const publicKey = key.getPublicKey(); + + try { + jwt.verify(bearerToken, publicKey, this.verifyOptions); + } catch { + return null; + } + + const { payload } = decodedToken; + if (typeof payload === 'string') { + return null; + } + + return { + id: payload['sub'] ?? payload['oid'], + email: payload['emails'][0], + firstName: payload['given_name'], + lastName: payload['family_name'], + organization: payload['organization'], + }; + } +} diff --git a/libs/api/auth/src/lib/auth-strategies/verify-auth-user.strategy.ts b/libs/api/auth/src/lib/auth-strategies/verify-auth-user.strategy.ts new file mode 100644 index 00000000..0936449c --- /dev/null +++ b/libs/api/auth/src/lib/auth-strategies/verify-auth-user.strategy.ts @@ -0,0 +1,10 @@ +import { Request } from 'express'; + +import { AuthUser } from '@kordis/shared/auth'; + +export abstract class VerifyAuthUserStrategy { + /* + * Returns the AuthUser if the user is authenticated, otherwise null. + */ + abstract verifyUserFromRequest(req: Request): Promise; +} diff --git a/libs/api/auth/src/lib/auth-user-extractor-strategies/extract-user-from-ms-principle-header.spec.ts b/libs/api/auth/src/lib/auth-strategies/verify-dev-bearer.strategy.spec.ts similarity index 61% rename from libs/api/auth/src/lib/auth-user-extractor-strategies/extract-user-from-ms-principle-header.spec.ts rename to libs/api/auth/src/lib/auth-strategies/verify-dev-bearer.strategy.spec.ts index e9efeda9..289ebd99 100644 --- a/libs/api/auth/src/lib/auth-user-extractor-strategies/extract-user-from-ms-principle-header.spec.ts +++ b/libs/api/auth/src/lib/auth-strategies/verify-dev-bearer.strategy.spec.ts @@ -2,28 +2,25 @@ import { createMock } from '@golevelup/ts-jest'; import { KordisRequest } from '@kordis/api/shared'; -import { - AuthUserExtractorStrategy, - ExtractUserFromMsPrincipleHeader, -} from './auth-user-extractor.strategy'; +import { VerifyAuthUserStrategy } from './verify-auth-user.strategy'; +import { VerifyDevBearerStrategy } from './verify-dev-bearer.strategy'; -describe('ExtractUserFromMsPrincipleHeader', () => { - let extractStrat: AuthUserExtractorStrategy; +describe('VerifyDevBearerStrategy', () => { + let extractStrat: VerifyAuthUserStrategy; beforeEach(() => { - extractStrat = new ExtractUserFromMsPrincipleHeader(); + extractStrat = new VerifyDevBearerStrategy(); }); - it('should return null if the authorization header is not present', () => { + it('should return null if the authorization header is not present', async () => { const req = createMock>({ headers: {}, }); - const result = extractStrat.getUserFromRequest(req); - expect(result).toBeNull(); + await expect(extractStrat.verifyUserFromRequest(req)).resolves.toBeNull(); }); - it('should extract user correctly from signed access token', () => { + it('should extract user correctly from signed access token', async () => { const headerValue = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJvaWQiOiJjMGNjNDQwNC03OTA3LTQ0ODAtODZkMy1iYTRiZmM1MTNjNmQiLCJzdWIiOiJjMGNjNDQwNC03OTA3LTQ0ODAtODZkMy1iYTRiZmM1MTNjNmQiLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVXNlciIsImVtYWlscyI6WyJ0ZXN0QHRpbW9ubWFzYmVyZy5jb20iXX0.9FXjgT037QkeE0KptQo3MzMriuXGzqCNfBDVEkWbJaA'; @@ -31,7 +28,7 @@ describe('ExtractUserFromMsPrincipleHeader', () => { headers: { authorization: headerValue }, }); - expect(extractStrat.getUserFromRequest(req)).toEqual({ + await expect(extractStrat.verifyUserFromRequest(req)).resolves.toEqual({ id: 'c0cc4404-7907-4480-86d3-ba4bfc513c6d', email: 'test@timonmasberg.com', firstName: 'Test', diff --git a/libs/api/auth/src/lib/auth-user-extractor-strategies/auth-user-extractor.strategy.ts b/libs/api/auth/src/lib/auth-strategies/verify-dev-bearer.strategy.ts similarity index 64% rename from libs/api/auth/src/lib/auth-user-extractor-strategies/auth-user-extractor.strategy.ts rename to libs/api/auth/src/lib/auth-strategies/verify-dev-bearer.strategy.ts index 8bd782ac..18ea8907 100644 --- a/libs/api/auth/src/lib/auth-user-extractor-strategies/auth-user-extractor.strategy.ts +++ b/libs/api/auth/src/lib/auth-strategies/verify-dev-bearer.strategy.ts @@ -2,16 +2,14 @@ import { Request } from 'express'; import { AuthUser } from '@kordis/shared/auth'; -export abstract class AuthUserExtractorStrategy { - abstract getUserFromRequest(req: Request): AuthUser | null; -} +import { VerifyAuthUserStrategy } from './verify-auth-user.strategy'; -export class ExtractUserFromMsPrincipleHeader extends AuthUserExtractorStrategy { - getUserFromRequest(req: Request): AuthUser | null { +export class VerifyDevBearerStrategy extends VerifyAuthUserStrategy { + verifyUserFromRequest(req: Request): Promise { const headerValue = req.headers['authorization']; if (!headerValue) { - return null; + return Promise.resolve(null); } const payloadBuffer = Buffer.from(headerValue.split('.')[1], 'base64'); const decodedToken = JSON.parse(payloadBuffer.toString()) as { @@ -23,12 +21,12 @@ export class ExtractUserFromMsPrincipleHeader extends AuthUserExtractorStrategy organization: string; }; - return { - id: decodedToken['oid'] || decodedToken['sub'], + return Promise.resolve({ + id: decodedToken['oid'] ?? decodedToken['sub'], email: decodedToken['emails'][0], firstName: decodedToken['given_name'], lastName: decodedToken['family_name'], organization: decodedToken['organization'], - }; + }); } } diff --git a/libs/api/auth/src/lib/auth.module.ts b/libs/api/auth/src/lib/auth.module.ts index 08d18124..be147391 100644 --- a/libs/api/auth/src/lib/auth.module.ts +++ b/libs/api/auth/src/lib/auth.module.ts @@ -1,22 +1,50 @@ -import { Module } from '@nestjs/common'; +import { DynamicModule, Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; -import { - AuthUserExtractorStrategy, - ExtractUserFromMsPrincipleHeader, -} from './auth-user-extractor-strategies/auth-user-extractor.strategy'; +import { VerifyAADB2CJWTStrategy } from './auth-strategies/verify-aadb2c-jwt.strategy'; +import { VerifyAuthUserStrategy } from './auth-strategies/verify-auth-user.strategy'; +import { VerifyDevBearerStrategy } from './auth-strategies/verify-dev-bearer.strategy'; import { AuthInterceptor } from './interceptors/auth.interceptor'; @Module({ providers: [ { - provide: AuthUserExtractorStrategy, - useClass: ExtractUserFromMsPrincipleHeader, + provide: VerifyAuthUserStrategy, + useClass: VerifyAADB2CJWTStrategy, }, + ], + exports: [VerifyAuthUserStrategy], +}) +class AADB2CAuthModule {} + +@Module({ + providers: [ { - provide: APP_INTERCEPTOR, - useClass: AuthInterceptor, + provide: VerifyAuthUserStrategy, + useClass: VerifyDevBearerStrategy, }, ], + exports: [VerifyAuthUserStrategy], }) -export class AuthModule {} +class DevAuthModule {} + +const AUTH_MODULES = Object.freeze({ + dev: DevAuthModule, + aadb2c: AADB2CAuthModule, +}); + +@Module({}) +export class AuthModule { + static forRoot(authProvider: keyof typeof AUTH_MODULES): DynamicModule { + return { + module: AuthModule, + imports: [AUTH_MODULES[authProvider]], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: AuthInterceptor, + }, + ], + }; + } +} diff --git a/libs/api/auth/src/lib/interceptors/auth.interceptor.spec.ts b/libs/api/auth/src/lib/interceptors/auth.interceptor.spec.ts index a788dfdd..e1a7ebab 100644 --- a/libs/api/auth/src/lib/interceptors/auth.interceptor.spec.ts +++ b/libs/api/auth/src/lib/interceptors/auth.interceptor.spec.ts @@ -6,18 +6,18 @@ import { KordisRequest } from '@kordis/api/shared'; import { createGqlContextForRequest } from '@kordis/api/test-helpers'; import { AuthUser } from '@kordis/shared/auth'; -import { AuthUserExtractorStrategy } from '../auth-user-extractor-strategies/auth-user-extractor.strategy'; +import { VerifyAuthUserStrategy } from '../auth-strategies/verify-auth-user.strategy'; import { AuthInterceptor } from './auth.interceptor'; describe('AuthInterceptor', () => { - let mockAuthUserExtractor: AuthUserExtractorStrategy; + let mockAuthUserExtractor: VerifyAuthUserStrategy; let service: AuthInterceptor; beforeEach(() => { - mockAuthUserExtractor = new (class extends AuthUserExtractorStrategy { + mockAuthUserExtractor = new (class extends VerifyAuthUserStrategy { // eslint-disable-next-line @typescript-eslint/no-unused-vars - getUserFromRequest(req: KordisRequest): AuthUser | null { - return null; + verifyUserFromRequest(req: KordisRequest): Promise { + return Promise.resolve(null); } })(); service = new AuthInterceptor(mockAuthUserExtractor); @@ -29,12 +29,12 @@ describe('AuthInterceptor', () => { it('should throw unauthorized http exception', async () => { jest - .spyOn(mockAuthUserExtractor, 'getUserFromRequest') - .mockReturnValue(null); + .spyOn(mockAuthUserExtractor, 'verifyUserFromRequest') + .mockResolvedValueOnce(null); await expect( firstValueFrom( - service.intercept( + await service.intercept( createGqlContextForRequest(createMock()), createMock(), ), @@ -43,13 +43,15 @@ describe('AuthInterceptor', () => { }); it('should continue request pipeline', async () => { - jest.spyOn(mockAuthUserExtractor, 'getUserFromRequest').mockReturnValue({ - id: '123', - firstName: 'foo', - lastName: 'bar', - email: 'foo@bar.de', - organization: 'testorg', - }); + jest + .spyOn(mockAuthUserExtractor, 'verifyUserFromRequest') + .mockResolvedValue({ + id: '123', + firstName: 'foo', + lastName: 'bar', + email: 'foo@bar.de', + organization: 'testorg', + }); const handler = createMock({ handle(): Observable { @@ -60,13 +62,13 @@ describe('AuthInterceptor', () => { const gqlCtx = createGqlContextForRequest(createMock()); await expect( - firstValueFrom(service.intercept(gqlCtx, handler)), + firstValueFrom(await service.intercept(gqlCtx, handler)), ).resolves.toBeTruthy(); const httpCtx = createGqlContextForRequest(createMock()); await expect( - firstValueFrom(service.intercept(httpCtx, handler)), + firstValueFrom(await service.intercept(httpCtx, handler)), ).resolves.toBeTruthy(); }); }); diff --git a/libs/api/auth/src/lib/interceptors/auth.interceptor.ts b/libs/api/auth/src/lib/interceptors/auth.interceptor.ts index 0df53628..93ac11e9 100644 --- a/libs/api/auth/src/lib/interceptors/auth.interceptor.ts +++ b/libs/api/auth/src/lib/interceptors/auth.interceptor.ts @@ -12,15 +12,18 @@ import { Observable, throwError } from 'rxjs'; import { KordisLogger } from '@kordis/api/observability'; import { KordisGqlContext, KordisRequest } from '@kordis/api/shared'; -import { AuthUserExtractorStrategy } from '../auth-user-extractor-strategies/auth-user-extractor.strategy'; +import { VerifyAuthUserStrategy } from '../auth-strategies/verify-auth-user.strategy'; @Injectable() export class AuthInterceptor implements NestInterceptor { private readonly logger: KordisLogger = new Logger(AuthInterceptor.name); - constructor(private readonly authUserExtractor: AuthUserExtractorStrategy) {} + constructor(private readonly authUserExtractor: VerifyAuthUserStrategy) {} - intercept(context: ExecutionContext, next: CallHandler): Observable { + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { let req: KordisRequest; if (context.getType() === 'graphql') { const ctx = GqlExecutionContext.create(context); @@ -29,16 +32,17 @@ export class AuthInterceptor implements NestInterceptor { req = context.switchToHttp().getRequest(); } - const possibleAuthUser = this.authUserExtractor.getUserFromRequest(req); + const possibleAuthUser = await this.authUserExtractor.verifyUserFromRequest( + req, + ); if (!possibleAuthUser) { - this.logger.warn('Request without any extractable auth user', { + this.logger.warn('Request with invalid JWT', { headers: req.headers, body: req.body, params: req.params, }); - // This is just intended to be a fallback, as we currently only aim to support running the API behind an OAuth Proxy - // You could write a custom auth user strategy which handles your auth process and return null if unauthorized + return throwError(() => new UnauthorizedException()); } diff --git a/libs/api/auth/tsconfig.json b/libs/api/auth/tsconfig.json index 8122543a..f07bbf22 100644 --- a/libs/api/auth/tsconfig.json +++ b/libs/api/auth/tsconfig.json @@ -7,7 +7,8 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true }, "files": [], "include": [], diff --git a/package-lock.json b/package-lock.json index 28259733..c1b20c13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,8 @@ "class-validator": "^0.14.0", "graphql": "^16.6.0", "graphql-sse": "^2.1.1", + "jsonwebtoken": "^9.0.1", + "jwks-rsa": "^3.0.1", "mongoose": "^7.0.3", "pino": "^8.15.0", "pino-pretty": "^10.2.0", @@ -10436,6 +10438,14 @@ "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -12304,6 +12314,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -14085,6 +14100,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -19435,6 +19458,14 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/jose": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", + "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -19642,6 +19673,56 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", + "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==", + "dependencies": { + "jws": "^3.2.2", + "lodash": "^4.17.21", + "ms": "^2.1.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.0.1.tgz", + "integrity": "sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==", + "dependencies": { + "@types/express": "^4.17.14", + "@types/jsonwebtoken": "^9.0.0", + "debug": "^4.3.4", + "jose": "^4.10.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", @@ -19901,6 +19982,11 @@ "node": ">=10" } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "node_modules/lines-and-columns": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", @@ -19961,6 +20047,11 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -20152,6 +20243,29 @@ "node": ">=12" } }, + "node_modules/lru-memoizer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz", + "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -23252,6 +23366,11 @@ "dev": true, "optional": true }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index a9fb1408..241b4282 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,8 @@ "class-validator": "^0.14.0", "graphql": "^16.6.0", "graphql-sse": "^2.1.1", + "jsonwebtoken": "^9.0.1", + "jwks-rsa": "^3.0.1", "mongoose": "^7.0.3", "pino": "^8.15.0", "pino-pretty": "^10.2.0", From 6910070b2ab9c03f119c34ae6353d6db3c59370e Mon Sep 17 00:00:00 2001 From: Timon Masberg Date: Wed, 13 Dec 2023 17:51:44 +0100 Subject: [PATCH 3/7] style: run prettier with new config --- .prettierrc | 9 ++------- apps/spa-e2e/playwright.config.ts | 2 +- apps/spa/src/app/app.module.ts | 4 ++-- .../auth-strategies/verify-aadb2c-jwt.strategy.spec.ts | 4 ++-- libs/api/auth/src/lib/interceptors/auth.interceptor.ts | 5 ++--- libs/spa/auth/src/lib/components/auth.component.ts | 2 +- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/.prettierrc b/.prettierrc index 1913acf2..6f60575c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,15 +6,10 @@ "proseWrap": "always", "overrides": [ { - "files": [ - "*.ts" - ], + "files": ["*.ts"], "options": { "parser": "typescript", - "importOrder": [ - "^@kordis/(.*)$", - "^[./]" - ], + "importOrder": ["^@kordis/(.*)$", "^[./]"], "importOrderSeparation": true, "importOrderSortSpecifiers": true, "importOrderParserPlugins": [ diff --git a/apps/spa-e2e/playwright.config.ts b/apps/spa-e2e/playwright.config.ts index a462a3a2..f16c0998 100644 --- a/apps/spa-e2e/playwright.config.ts +++ b/apps/spa-e2e/playwright.config.ts @@ -1,6 +1,6 @@ +import { nxE2EPreset } from '@nx/playwright/preset'; import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; -import { nxE2EPreset } from '@nx/playwright/preset'; const baseURL = process.env.E2E_BASE_URL || 'http://localhost:4200/'; diff --git a/apps/spa/src/app/app.module.ts b/apps/spa/src/app/app.module.ts index d47fbd28..45252b84 100644 --- a/apps/spa/src/app/app.module.ts +++ b/apps/spa/src/app/app.module.ts @@ -24,7 +24,7 @@ import routes from './routes'; ? AuthModule.forRoot( environment.oauth.config, environment.oauth.discoveryDocumentUrl, - ) + ) : DevAuthModule.forRoot(), // for now, we accept that we have the sentry module and dependencies in our dev bundle as well environment.sentryKey @@ -32,7 +32,7 @@ import routes from './routes'; environment.sentryKey, environment.environmentName, environment.releaseVersion, - ) + ) : NoopObservabilityModule.forRoot(), ], providers: [], diff --git a/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.spec.ts b/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.spec.ts index 807d4251..4fb25c40 100644 --- a/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.spec.ts +++ b/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.spec.ts @@ -66,7 +66,7 @@ describe('VerifyAADB2CJWTStrategy', () => { emails: ['foo@bar.de'], organization: 'testorg', }, - } as any), + }) as any, ); const verifySpy = jest.spyOn(jwt, 'verify').mockReturnValueOnce(undefined); @@ -108,7 +108,7 @@ describe('VerifyAADB2CJWTStrategy', () => { () => ({ header: { kid: 'mockKid' }, - } as any), + }) as any, ); jest.spyOn(jwt, 'verify').mockImplementationOnce(() => { diff --git a/libs/api/auth/src/lib/interceptors/auth.interceptor.ts b/libs/api/auth/src/lib/interceptors/auth.interceptor.ts index 93ac11e9..93b712a0 100644 --- a/libs/api/auth/src/lib/interceptors/auth.interceptor.ts +++ b/libs/api/auth/src/lib/interceptors/auth.interceptor.ts @@ -32,9 +32,8 @@ export class AuthInterceptor implements NestInterceptor { req = context.switchToHttp().getRequest(); } - const possibleAuthUser = await this.authUserExtractor.verifyUserFromRequest( - req, - ); + const possibleAuthUser = + await this.authUserExtractor.verifyUserFromRequest(req); if (!possibleAuthUser) { this.logger.warn('Request with invalid JWT', { diff --git a/libs/spa/auth/src/lib/components/auth.component.ts b/libs/spa/auth/src/lib/components/auth.component.ts index 9a68b221..e1205aa1 100644 --- a/libs/spa/auth/src/lib/components/auth.component.ts +++ b/libs/spa/auth/src/lib/components/auth.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { map, Observable } from 'rxjs'; +import { Observable, map } from 'rxjs'; import { AUTH_SERVICE, AuthService } from '../services/auth-service'; From 5ba206b3868d74748240286a1c1698fa64ddbd21 Mon Sep 17 00:00:00 2001 From: Timon Masberg Date: Sat, 20 Jan 2024 17:00:01 +0100 Subject: [PATCH 4/7] chore(api): set dev modules via forRoot based on node env and github actions flag --- apps/api/src/app/app.module.ts | 14 +- libs/api/auth/src/lib/auth.module.ts | 3 +- libs/api/observability/src/index.ts | 3 +- .../src/lib/dev-observability.module.ts | 7 +- .../src/lib/observability.module.ts | 28 ++++ .../src/lib/sentry-observability.module.ts | 5 - package-lock.json | 142 +++++++----------- 7 files changed, 89 insertions(+), 113 deletions(-) create mode 100644 libs/api/observability/src/lib/observability.module.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 287e6ffe..3216f977 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -8,10 +8,7 @@ import { AutomapperModule } from '@timonmasberg/automapper-nestjs'; import * as path from 'path'; import { AuthModule } from '@kordis/api/auth'; -import { - DevObservabilityModule, - SentryObservabilityModule, -} from '@kordis/api/observability'; +import { ObservabilityModule } from '@kordis/api/observability'; import { OrganizationModule } from '@kordis/api/organization'; import { SharedKernel, errorFormatterFactory } from '@kordis/api/shared'; @@ -20,13 +17,14 @@ import { AppService } from './app.service'; import { GraphqlSubscriptionsController } from './controllers/graphql-subscriptions.controller'; import environment from './environment'; +// todo: narrow this down, we should have either an explicit way of defining when to use what (currently hard, since ConfigService not available in forRoot), or we should have an environment variable that defines the environment +const isProdEnv = + process.env.NODE_ENV === 'production' && !process.env.GITHUB_ACTIONS; const FEATURE_MODULES = [OrganizationModule]; const UTILITY_MODULES = [ SharedKernel, - AuthModule.forRoot('dev'), // todo: this needs to be changed to aadb2c when we move to onpremise - ...(process.env.NODE_ENV === 'production' && !process.env.GITHUB_ACTIONS - ? [SentryObservabilityModule] - : [DevObservabilityModule]), + AuthModule.forRoot(isProdEnv ? 'aadb2c' : 'dev'), + ObservabilityModule.forRoot(isProdEnv ? 'sentry' : 'dev'), ]; @Module({ diff --git a/libs/api/auth/src/lib/auth.module.ts b/libs/api/auth/src/lib/auth.module.ts index be147391..c06edba8 100644 --- a/libs/api/auth/src/lib/auth.module.ts +++ b/libs/api/auth/src/lib/auth.module.ts @@ -13,7 +13,6 @@ import { AuthInterceptor } from './interceptors/auth.interceptor'; useClass: VerifyAADB2CJWTStrategy, }, ], - exports: [VerifyAuthUserStrategy], }) class AADB2CAuthModule {} @@ -24,7 +23,6 @@ class AADB2CAuthModule {} useClass: VerifyDevBearerStrategy, }, ], - exports: [VerifyAuthUserStrategy], }) class DevAuthModule {} @@ -39,6 +37,7 @@ export class AuthModule { return { module: AuthModule, imports: [AUTH_MODULES[authProvider]], + exports: [VerifyAuthUserStrategy], providers: [ { provide: APP_INTERCEPTOR, diff --git a/libs/api/observability/src/index.ts b/libs/api/observability/src/index.ts index f30ea6d0..41c341f4 100644 --- a/libs/api/observability/src/index.ts +++ b/libs/api/observability/src/index.ts @@ -1,4 +1,3 @@ -export * from './lib/sentry-observability.module'; -export * from './lib/dev-observability.module'; +export * from './lib/observability.module'; export * from './lib/decorators/trace.decorator'; export * from './lib/services/kordis-logger.interface'; diff --git a/libs/api/observability/src/lib/dev-observability.module.ts b/libs/api/observability/src/lib/dev-observability.module.ts index 99490b77..c03ccc7f 100644 --- a/libs/api/observability/src/lib/dev-observability.module.ts +++ b/libs/api/observability/src/lib/dev-observability.module.ts @@ -1,7 +1,6 @@ -import { Logger, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { KORDIS_LOGGER_SERVICE } from './services/kordis-logger-service.interface'; -import { KordisLoggerImpl } from './services/kordis.logger'; import { PinoLogger } from './services/pino-logger.service'; @Module({ @@ -10,10 +9,6 @@ import { PinoLogger } from './services/pino-logger.service'; provide: KORDIS_LOGGER_SERVICE, useValue: new PinoLogger(true), }, - { - provide: Logger, - useClass: KordisLoggerImpl, - }, ], }) export class DevObservabilityModule {} diff --git a/libs/api/observability/src/lib/observability.module.ts b/libs/api/observability/src/lib/observability.module.ts new file mode 100644 index 00000000..a447b61d --- /dev/null +++ b/libs/api/observability/src/lib/observability.module.ts @@ -0,0 +1,28 @@ +import { DynamicModule, Logger, Module } from '@nestjs/common'; + +import { DevObservabilityModule } from './dev-observability.module'; +import { SentryObservabilityModule } from './sentry-observability.module'; +import { KordisLoggerImpl } from './services/kordis.logger'; + +const OBSERVABILITY_MODULES = Object.freeze({ + dev: DevObservabilityModule, + sentry: SentryObservabilityModule, +}); + +@Module({}) +export class ObservabilityModule { + static forRoot( + observabilityProvider: keyof typeof OBSERVABILITY_MODULES, + ): DynamicModule { + return { + module: ObservabilityModule, + imports: [OBSERVABILITY_MODULES[observabilityProvider]], + providers: [ + { + provide: Logger, + useClass: KordisLoggerImpl, + }, + ], + }; + } +} diff --git a/libs/api/observability/src/lib/sentry-observability.module.ts b/libs/api/observability/src/lib/sentry-observability.module.ts index 31996abe..a727a113 100644 --- a/libs/api/observability/src/lib/sentry-observability.module.ts +++ b/libs/api/observability/src/lib/sentry-observability.module.ts @@ -8,17 +8,12 @@ import { SentryOTelUserContextInterceptor } from './interceptors/sentry-otel-use import oTelSDK from './oTelSdk'; import { KORDIS_LOGGER_SERVICE } from './services/kordis-logger-service.interface'; import { KordisLogger } from './services/kordis-logger.interface'; -import { KordisLoggerImpl } from './services/kordis.logger'; import { SentryLogger } from './services/sentry-logger.service'; import { wrapProvidersWithTracingSpans } from './trace-wrapper'; // This Module must come after the AuthModule, because it depends on the use set by the AuthInterceptor @Module({ providers: [ - { - provide: Logger, - useClass: KordisLoggerImpl, - }, { provide: KORDIS_LOGGER_SERVICE, useClass: SentryLogger, diff --git a/package-lock.json b/package-lock.json index 1a071260..081d86e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3807,12 +3807,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -3828,18 +3822,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4134,6 +4116,15 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -4147,6 +4138,19 @@ "node": ">=8" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -9737,6 +9741,28 @@ "node": ">=14.15.0" } }, + "node_modules/@yarnpkg/parsers/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@yarnpkg/parsers/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@zkochan/js-yaml": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz", @@ -9749,12 +9775,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@zkochan/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -10050,13 +10070,10 @@ "dev": true }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/aria-query": { "version": "5.3.0", @@ -12156,24 +12173,6 @@ "typescript": ">=4" } }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -13990,12 +13989,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -14094,18 +14087,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -18986,13 +18967,12 @@ "dev": true }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -21238,12 +21218,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/nx/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/nx/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -21319,18 +21293,6 @@ "node": ">=8" } }, - "node_modules/nx/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/nx/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", From 41a5ed478689fc50aaeba029dc7a2345f89ef2e4 Mon Sep 17 00:00:00 2001 From: Timon Masberg Date: Sat, 20 Jan 2024 17:02:30 +0100 Subject: [PATCH 5/7] style: run prettier --- apps/api/tsconfig.json | 10 +++++----- apps/spa-e2e/tsconfig.json | 6 +++--- apps/spa/tsconfig.json | 14 +++++++------- libs/api/auth/tsconfig.json | 10 +++++----- libs/api/observability/tsconfig.json | 10 +++++----- libs/api/organization/tsconfig.json | 10 +++++----- libs/api/shared/tsconfig.json | 10 +++++----- libs/api/test-helpers/tsconfig.json | 8 ++++---- libs/shared/auth/tsconfig.json | 8 ++++---- libs/spa/auth/tsconfig.json | 12 ++++++------ libs/spa/observability/tsconfig.json | 12 ++++++------ 11 files changed, 55 insertions(+), 55 deletions(-) diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 0a105ad1..c8ffb024 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -4,11 +4,11 @@ "include": [], "references": [ { - "path": "./tsconfig.app.json" + "path": "./tsconfig.app.json", }, { - "path": "./tsconfig.spec.json" - } + "path": "./tsconfig.spec.json", + }, ], "compilerOptions": { "esModuleInterop": true, @@ -18,6 +18,6 @@ "noPropertyAccessFromIndexSignature": false, "noImplicitOverride": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - } + "noFallthroughCasesInSwitch": true, + }, } diff --git a/apps/spa-e2e/tsconfig.json b/apps/spa-e2e/tsconfig.json index 08841a7f..ac17088b 100644 --- a/apps/spa-e2e/tsconfig.json +++ b/apps/spa-e2e/tsconfig.json @@ -4,7 +4,7 @@ "include": [], "references": [ { - "path": "./tsconfig.e2e.json" - } - ] + "path": "./tsconfig.e2e.json", + }, + ], } diff --git a/apps/spa/tsconfig.json b/apps/spa/tsconfig.json index e01cf19b..a2e89bc5 100644 --- a/apps/spa/tsconfig.json +++ b/apps/spa/tsconfig.json @@ -7,26 +7,26 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.app.json" + "path": "./tsconfig.app.json", }, { - "path": "./tsconfig.spec.json" + "path": "./tsconfig.spec.json", }, { - "path": "./tsconfig.editor.json" - } + "path": "./tsconfig.editor.json", + }, ], "extends": "../../tsconfig.base.json", "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, - "strictTemplates": true - } + "strictTemplates": true, + }, } diff --git a/libs/api/auth/tsconfig.json b/libs/api/auth/tsconfig.json index f07bbf22..b8b1c714 100644 --- a/libs/api/auth/tsconfig.json +++ b/libs/api/auth/tsconfig.json @@ -8,16 +8,16 @@ "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "esModuleInterop": true + "esModuleInterop": true, }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.spec.json" - } - ] + "path": "./tsconfig.spec.json", + }, + ], } diff --git a/libs/api/observability/tsconfig.json b/libs/api/observability/tsconfig.json index f1edc0cf..4f844ad5 100644 --- a/libs/api/observability/tsconfig.json +++ b/libs/api/observability/tsconfig.json @@ -8,16 +8,16 @@ "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "strictPropertyInitialization": false + "strictPropertyInitialization": false, }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.spec.json" - } - ] + "path": "./tsconfig.spec.json", + }, + ], } diff --git a/libs/api/organization/tsconfig.json b/libs/api/organization/tsconfig.json index 25f7201d..507ea363 100644 --- a/libs/api/organization/tsconfig.json +++ b/libs/api/organization/tsconfig.json @@ -1,16 +1,16 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "module": "commonjs" + "module": "commonjs", }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.spec.json" - } - ] + "path": "./tsconfig.spec.json", + }, + ], } diff --git a/libs/api/shared/tsconfig.json b/libs/api/shared/tsconfig.json index 5c85cec6..c4fba095 100644 --- a/libs/api/shared/tsconfig.json +++ b/libs/api/shared/tsconfig.json @@ -7,16 +7,16 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true, - "strictPropertyInitialization": false + "strictPropertyInitialization": false, }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.spec.json" - } - ] + "path": "./tsconfig.spec.json", + }, + ], } diff --git a/libs/api/test-helpers/tsconfig.json b/libs/api/test-helpers/tsconfig.json index 9bde1959..dc3cd7d5 100644 --- a/libs/api/test-helpers/tsconfig.json +++ b/libs/api/test-helpers/tsconfig.json @@ -1,13 +1,13 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "module": "commonjs" + "module": "commonjs", }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.lib.json" - } - ] + "path": "./tsconfig.lib.json", + }, + ], } diff --git a/libs/shared/auth/tsconfig.json b/libs/shared/auth/tsconfig.json index f2400abe..c66a1b77 100644 --- a/libs/shared/auth/tsconfig.json +++ b/libs/shared/auth/tsconfig.json @@ -7,13 +7,13 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.lib.json" - } - ] + "path": "./tsconfig.lib.json", + }, + ], } diff --git a/libs/spa/auth/tsconfig.json b/libs/spa/auth/tsconfig.json index 5cf0a165..f390c6ab 100644 --- a/libs/spa/auth/tsconfig.json +++ b/libs/spa/auth/tsconfig.json @@ -7,23 +7,23 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.spec.json" - } + "path": "./tsconfig.spec.json", + }, ], "extends": "../../../tsconfig.base.json", "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, - "strictTemplates": true - } + "strictTemplates": true, + }, } diff --git a/libs/spa/observability/tsconfig.json b/libs/spa/observability/tsconfig.json index 5cf0a165..f390c6ab 100644 --- a/libs/spa/observability/tsconfig.json +++ b/libs/spa/observability/tsconfig.json @@ -7,23 +7,23 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.lib.json" + "path": "./tsconfig.lib.json", }, { - "path": "./tsconfig.spec.json" - } + "path": "./tsconfig.spec.json", + }, ], "extends": "../../../tsconfig.base.json", "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, - "strictTemplates": true - } + "strictTemplates": true, + }, } From 3a6be90862facd430237eebccaec3538924e1765 Mon Sep 17 00:00:00 2001 From: Timon Masberg Date: Thu, 1 Feb 2024 15:27:24 +0100 Subject: [PATCH 6/7] chore(api): init prod dependencies only in next and prod environments --- apps/api/src/app/app.module.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 04a56f96..006b4e4e 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -18,14 +18,15 @@ import { GraphqlSubscriptionsController } from './controllers/graphql-subscripti import { HealthCheckController } from './controllers/health-check.controller'; import environment from './environment'; -// todo: narrow this down, we should have either an explicit way of defining when to use what (currently hard, since ConfigService not available in forRoot), or we should have an environment variable that defines the environment -const isProdEnv = - process.env.NODE_ENV === 'production' && !process.env.GITHUB_ACTIONS; +const isNextOrProdEnv = ['next', 'prod'].includes( + process.env.ENVIRONMENT_NAME ?? '', +); + const FEATURE_MODULES = [OrganizationModule]; const UTILITY_MODULES = [ SharedKernel, - AuthModule.forRoot(isProdEnv ? 'aadb2c' : 'dev'), - ObservabilityModule.forRoot(isProdEnv ? 'sentry' : 'dev'), + AuthModule.forRoot(isNextOrProdEnv ? 'aadb2c' : 'dev'), + ObservabilityModule.forRoot(isNextOrProdEnv ? 'sentry' : 'dev'), ]; @Module({ From 34132dfc0d7d72f4764e43beaada38a161b9d527 Mon Sep 17 00:00:00 2001 From: Timon Masberg Date: Tue, 13 Feb 2024 13:52:38 +0100 Subject: [PATCH 7/7] refactor(api-auth): catch exceptions during decoding and verifying the token --- .../verify-aadb2c-jwt.strategy.ts | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.ts b/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.ts index ca6b75dd..469e04b8 100644 --- a/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.ts +++ b/libs/api/auth/src/lib/auth-strategies/verify-aadb2c-jwt.strategy.ts @@ -1,9 +1,10 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Request } from 'express'; import * as jwt from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; +import { KordisLogger } from '@kordis/api/observability'; import { AuthUser } from '@kordis/shared/auth'; import { VerifyAuthUserStrategy } from './verify-auth-user.strategy'; @@ -23,6 +24,9 @@ declare module 'jsonwebtoken' { export class VerifyAADB2CJWTStrategy extends VerifyAuthUserStrategy { private readonly client: jwksClient.JwksClient; private readonly verifyOptions: jwt.VerifyOptions; + private readonly logger: KordisLogger = new Logger( + VerifyAADB2CJWTStrategy.name, + ); constructor(config: ConfigService) { super(); @@ -48,21 +52,26 @@ export class VerifyAADB2CJWTStrategy extends VerifyAuthUserStrategy { if (!authHeaderValue) { return null; } - const bearerToken = authHeaderValue.split(' ')[1]; - const decodedToken = jwt.decode(bearerToken, { - complete: true, - }); - if (!decodedToken) { - return null; - } + let decodedToken: jwt.Jwt; + try { + const bearerToken = authHeaderValue.split(' ')[1]; + const possibleDecodedToken = jwt.decode(bearerToken, { + complete: true, + }); + if (!possibleDecodedToken) { + return null; + } + decodedToken = possibleDecodedToken; - const key = await this.client.getSigningKey(decodedToken.header.kid); - const publicKey = key.getPublicKey(); + const key = await this.client.getSigningKey(decodedToken.header.kid); + const publicKey = key.getPublicKey(); - try { jwt.verify(bearerToken, publicKey, this.verifyOptions); - } catch { + } catch (error) { + this.logger.warn('Failed to decode or verify bearer token', { + error, + }); return null; }