import request from 'supertest'; import { Test } from '@nestjs/testing'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import jwt from 'jsonwebtoken'; import { Body, Controller, Get } from '@nestjs/common'; import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; import { AuthenticateCondition, Authenticated, RequireAllPermissions, RequireSomePermission, } from '../decorators/authentication.decorator'; import { AuthenticationGuard } from './authentication.guard'; import { PERMISSIONS_GROUPS } from './permissions.enum'; import { AuthClientService } from '../modules/auth/auth.service'; import ErrorCodes from '../utils/errorCodes'; import { ApiKeyService } from 'src/modules/api-key/api-key.service'; const logger = { info: (...args) => args, error: (...args) => args, }; @Controller('no-class-auth') class NoClassAuthController { @Get('body') async getBody(@Body() body) { return { body }; } @Get('authenticated') @Authenticated() async mustBeAuthenticated(@Body() body) { return { body }; } @Get('required-permission') @RequireSomePermission(PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN) async requiredPermission(@Body() body) { return { body }; } @Get('has-all-permissions') @RequireAllPermissions( PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN, PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE, ) async hasAllPermissions(@Body() body) { return { body }; } @Get('has-some-permission') @RequireSomePermission( PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN, PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE, ) async hasSomePermission(@Body() body) { return { body }; } } @Controller('class-auth-condition') @AuthenticateCondition((req) => req.get('x-on-class') === 'ok') @RequireSomePermission(PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE) class ClassAuthConditionController { @Get('body') async getBody(@Body() body) { return { body }; } @Get('authenticated') @Authenticated() async mustBeAuthenticated(@Body() body) { return { body }; } @Get('required-permission') @AuthenticateCondition((req) => req.get('x-on-route') === 'ok') @RequireSomePermission(PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN) async requiredPermission(@Body() body) { return { body }; } } describe('authentication.guard', () => { let app: INestApplication; const jwtSecretA = { kid: 'token-a-testing-shared-key', pem: '$tr0ng-SH4Red-secr3t!!!~gl0ba1~]', }; const jwtSecretB = { kid: 'token-b', pem: 'another-shared-token', }; const fakeUserPayload = { user_id: 'd50d33c7-6c2b-463c-861f-e21667e7c125', username: 'super.admin', customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11', customer_name: 'dadosfera', customer_tier: 'BASIC', customer_modules: [] }; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ providers: [ { provide: AuthClientService, useValue: { getPublicKeys: async () => ({ keys: [jwtSecretA, jwtSecretB], }), }, }, { provide: DadosferaLogger, useValue: { logger }, }, { provide: APP_GUARD, useClass: AuthenticationGuard, }, { provide: ApiKeyService, useValue: { get: () => Promise.resolve(null) } } ], controllers: [NoClassAuthController, ClassAuthConditionController], }).compile(); app = moduleRef.createNestApplication(); await app.init(); }); afterAll(async () => { await app.close(); }); function AssertBodyNoAuth(res: request.Response) { expect(res.statusCode).toBe(HttpStatus.OK); expect(res.body).toStrictEqual({ body: {} }); } function AssertBodyWithAuth(res: request.Response) { expect(res.statusCode).toBe(HttpStatus.OK); expect(res.body).toStrictEqual({ body: { info: { customer: 'dadosfera', customer_id: '9d18e8ae-24b9-41a3-9e8f-a25ce57555b11', customer_tier: 'BASIC', user_id: 'd50d33c7-6c2b-463c-861f-e21667e7c125', }, }, }); } function AssertUnauthorized(res: request.Response) { expect(res.statusCode).toBe(HttpStatus.UNAUTHORIZED); expect(res.body).toStrictEqual({ code: ErrorCodes.AUTH.UNAUTHORIZED, error: 'Não autenticado', message: 'É necessário estar logado para realizar essa operação', statusCode: HttpStatus.UNAUTHORIZED, }); } function AssertForbidden(res: request.Response) { expect(res.statusCode).toBe(HttpStatus.FORBIDDEN); expect(res.body).toStrictEqual({ code: ErrorCodes.AUTH.FORBIDDEN, error: 'Não autorizado', message: 'Você não tem permissões suficientes para realizar essa operação', statusCode: HttpStatus.FORBIDDEN, }); } function NoClassAuthTest(accessToken: string, tokenName: string[]) { const route = '/no-class-auth'; describe(`${route}, ${tokenName?.length ? tokenName : 'none'} auth`, () => { it('should GET /body with user data (if available)', async () => { const res = await request(app.getHttpServer()) .get(`${route}/body`) .set({ Authorization: accessToken }); if (tokenName?.length) { AssertBodyWithAuth(res); } else { AssertBodyNoAuth(res); } }); it('should GET /authenticated if authenticated', async () => { const res = await request(app.getHttpServer()) .get(`${route}/authenticated`) .set({ Authorization: accessToken }); if (tokenName?.length) { AssertBodyWithAuth(res); } else { AssertUnauthorized(res); } }); it('should GET /required-permission if authenticated', async () => { const res = await request(app.getHttpServer()) .get(`${route}/required-permission`) .set({ Authorization: accessToken }); // zendesk required if (tokenName?.includes('zendesk')) { AssertBodyWithAuth(res); } else if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); it('should GET /has-all-permissions if authenticated', async () => { const res = await request(app.getHttpServer()) .get(`${route}/has-all-permissions`) .set({ Authorization: accessToken }); // zendesk and metabase if (tokenName?.includes('zendesk') && tokenName?.includes('metabase')) { AssertBodyWithAuth(res); } else if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); it('should GET /has-some-permission if authenticated', async () => { const res = await request(app.getHttpServer()) .get(`${route}/has-some-permission`) .set({ Authorization: accessToken }); // zendesk or metabase if (tokenName?.includes('zendesk') || tokenName?.includes('metabase')) { AssertBodyWithAuth(res); } else if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); }); } function ClassAuthConditionTest(accessToken: string, tokenName: string[]) { const route = '/class-auth-condition'; describe(`${route}, ${tokenName?.length ? tokenName : 'none'} auth`, () => { describe('GET /body', () => { it('should GET /body if authenticated and with correct headers', async () => { const res = await request(app.getHttpServer()) .get(`${route}/body`) .set({ 'x-on-class': 'ok', Authorization: accessToken, }); // metabase required if (tokenName?.includes('metabase')) { AssertBodyWithAuth(res); } else if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); it('should not GET /body without correct headers', async () => { const res = await request(app.getHttpServer()) .get(`${route}/body`) .set({ Authorization: accessToken }); if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); }); describe('GET /authenticated', () => { it('should GET /authenticated if authenticated and with correct headers', async () => { const res = await request(app.getHttpServer()) .get(`${route}/authenticated`) .set({ 'x-on-class': 'ok', Authorization: accessToken, }); // metabase required if (tokenName?.includes('metabase')) { AssertBodyWithAuth(res); } else if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); it('should not GET /authenticated without correct class headers', async () => { const res = await request(app.getHttpServer()) .get(`${route}/authenticated`) .set({ Authorization: accessToken }); if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); }); describe('GET /required-permission', () => { it('should GET /required-permission if authenticated and with correct headers', async () => { const res = await request(app.getHttpServer()) .get(`${route}/required-permission`) .set({ 'x-on-class': 'ok', 'x-on-route': 'ok', Authorization: accessToken, }); // zendesk and metabase if ( tokenName?.includes('zendesk') && tokenName?.includes('metabase') ) { AssertBodyWithAuth(res); } else if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); it('should not GET /required-permission without correct route headers', async () => { const res = await request(app.getHttpServer()) .get(`${route}/required-permission`) .set({ 'x-on-class': 'ok', Authorization: accessToken, }); if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); it('should not GET /required-permission without correct class headers', async () => { const res = await request(app.getHttpServer()) .get(`${route}/required-permission`) .set({ 'x-on-route': 'ok', Authorization: accessToken, }); if (tokenName?.length) { AssertForbidden(res); } else { AssertUnauthorized(res); } }); }); }); } describe('token validation', () => { it('should accept token kid A', async () => { const Authorization = CreateToken([], { jwt: { kid: jwtSecretA.kid, pem: jwtSecretA.pem, }, }); const res = await request(app.getHttpServer()) .get('/no-class-auth/authenticated') .set({ Authorization, }); AssertBodyWithAuth(res); }); it('should accept token kid B', async () => { const Authorization = CreateToken([], { jwt: { kid: jwtSecretB.kid, pem: jwtSecretB.pem, }, }); const res = await request(app.getHttpServer()) .get('/no-class-auth/authenticated') .set({ Authorization, }); AssertBodyWithAuth(res); }); it('should not accept unknown kid', async () => { const Authorization = CreateToken([], { jwt: { kid: 'unknown-key-id', pem: jwtSecretA.pem, }, }); const res = await request(app.getHttpServer()) .get('/no-class-auth/authenticated') .set({ Authorization, }); AssertUnauthorized(res); }); }); function CreateToken(permissions, overrides?: { user?: any; jwt?: any }) { const tokenPayload = { ...fakeUserPayload, permissions: permissions.map(({ seqid }) => seqid), token_use: 'access', ...overrides?.user, }; return jwt.sign(tokenPayload, overrides?.jwt?.pem ?? jwtSecretA.pem, { keyid: overrides?.jwt?.kid ?? jwtSecretA.kid, }); } NoClassAuthTest(null, null); ClassAuthConditionTest(null, null); // const tokenZ = CreateToken([PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN]); // NoClassAuthTest(tokenZ, ['zendesk']); // ClassAuthConditionTest(tokenZ, ['zendesk']); // const tokenM = CreateToken([PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE]); // NoClassAuthTest(tokenM, ['metabase']); // ClassAuthConditionTest(tokenM, ['metabase']); // const tokenZM = CreateToken([ // PERMISSIONS_GROUPS.ZENDESK.permissions.OPEN, // PERMISSIONS_GROUPS.DATAVIZ.permissions.METABASE, // ]); // NoClassAuthTest(tokenZM, ['zendesk', 'metabase']); // ClassAuthConditionTest(tokenZM, ['zendesk', 'metabase']); });