From 5c4e694dadc8ba399e4c15142eee4acad3c0e7c9 Mon Sep 17 00:00:00 2001 From: Max Polsky Date: Wed, 15 Nov 2023 05:30:49 -0800 Subject: [PATCH] Enable JWT middleware (#468) * feat: new jwt middlware * test: new auth test * feat: unauthorized error --- .../test/e2e/app_auth.e2e._spec.ts | 36 ------------- .../test/e2e/app_auth.e2e.spec.ts | 43 ++++++++++++++++ .../src/lib/auth_test_support.ts | 50 +++++++++++-------- libs/velo-external-db-core/src/index.ts | 1 + libs/velo-external-db-core/src/router.ts | 16 +++--- .../src/spi-model/errors.ts | 6 +++ libs/velo-external-db-core/src/types.ts | 4 +- .../src/web/domain-to-spi-error-translator.ts | 4 ++ .../src/web/middleware-support.ts | 6 +-- 9 files changed, 95 insertions(+), 71 deletions(-) delete mode 100644 apps/velo-external-db/test/e2e/app_auth.e2e._spec.ts create mode 100644 apps/velo-external-db/test/e2e/app_auth.e2e.spec.ts diff --git a/apps/velo-external-db/test/e2e/app_auth.e2e._spec.ts b/apps/velo-external-db/test/e2e/app_auth.e2e._spec.ts deleted file mode 100644 index 5a582be11..000000000 --- a/apps/velo-external-db/test/e2e/app_auth.e2e._spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Uninitialized, gen } from '@wix-velo/test-commons' -import { initApp, teardownApp, dbTeardown, setupDb, currentDbImplementationName } from '../resources/e2e_resources' - - -describe(`Velo External DB authorization: ${currentDbImplementationName()}`, () => { - beforeAll(async() => { - await setupDb() - - await initApp() - }, 20000) - - afterAll(async() => { - await dbTeardown() - }, 20000) - - // each(['data/query', 'data/aggregate', 'data/insert', 'data/insert/bulk', 'data/get', 'data/update', - // 'data/update/bulk', 'data/remove', 'data/remove/bulk', 'data/count']) - // .test('should throw 401 on a request to %s without the appropriate role', async(api) => { - // return expect(() => axios.post(api, { collectionName: ctx.collectionName }, authVisitor)).rejects.toThrow('401') - // }) - - // test('wrong secretKey will throw an appropriate error with the right format', async() => { - // return expect(() => axios.post('/schemas/list', {}, authOwnerWithoutSecretKey)).rejects.toMatchObject(errorResponseWith(401, 'You are not authorized')) - // }) - - - const ctx = { - collectionName: Uninitialized, - } - - beforeEach(async() => { - ctx.collectionName = gen.randomCollectionName() - }) - - afterAll(async() => await teardownApp()) -}) diff --git a/apps/velo-external-db/test/e2e/app_auth.e2e.spec.ts b/apps/velo-external-db/test/e2e/app_auth.e2e.spec.ts new file mode 100644 index 000000000..b44fb57dd --- /dev/null +++ b/apps/velo-external-db/test/e2e/app_auth.e2e.spec.ts @@ -0,0 +1,43 @@ +import axios from 'axios' +import each from 'jest-each' +import { config } from '@wix-velo/velo-external-db-core' +import { authOwnerWithoutJwt, authOwnerWithWrongJwtPublicKey, authVisitor, authOwnerWithWrongAppId } from '@wix-velo/external-db-testkit' +import { initApp, teardownApp, dbTeardown, setupDb, currentDbImplementationName } from '../resources/e2e_resources' + +const axiosInstance = axios.create({ + baseURL: 'http://localhost:8080' +}) + + +describe(`Velo External DB authorization: ${currentDbImplementationName()}`, () => { + beforeAll(async() => { + await setupDb() + await initApp() + }, 20000) + + afterAll(async() => { + await dbTeardown() + }, 20000) + + describe('Role authorization', () => { + each(config.map(i => i.pathPrefix)).test('should throw 401 on a request to %s without the appropriate role', async(api) => { + return expect(axiosInstance.post(api, {}, authVisitor)).rejects.toThrow('401') + }) + }) + + describe('JWT authorization', () => { + test('should throw if the request is not singed with JWT token', async() => { + return expect(axiosInstance.post('data/insert', {}, authOwnerWithoutJwt)).rejects.toThrow('401') + }) + + test('should throw if the request is singed with the wrong public key', async() => { + return expect(axiosInstance.post('data/insert', {}, authOwnerWithWrongJwtPublicKey)).rejects.toThrow('401') + }) + + test('should throw if the request is signed with wrong app id', async() => { + return expect(axiosInstance.post('data/insert', {}, authOwnerWithWrongAppId)).rejects.toThrow('401') + }) + }) + + afterAll(async() => await teardownApp()) +}) diff --git a/libs/external-db-testkit/src/lib/auth_test_support.ts b/libs/external-db-testkit/src/lib/auth_test_support.ts index 071dab4f3..7a80c10ea 100644 --- a/libs/external-db-testkit/src/lib/auth_test_support.ts +++ b/libs/external-db-testkit/src/lib/auth_test_support.ts @@ -1,53 +1,61 @@ -import * as Chance from 'chance' import { AxiosRequestHeaders } from 'axios' import * as jwt from 'jsonwebtoken' import { authConfig } from '@wix-velo/test-commons' -const chance = Chance() const axios = require('axios').create({ baseURL: 'http://localhost:8080', }) -const allowedMetasite = chance.word() -const externalDatabaseId = chance.word() +const TOKEN_ISSUER = 'wix.com' export const authInit = () => { - process.env['ALLOWED_METASITES'] = allowedMetasite - process.env['EXTERNAL_DATABASE_ID'] = externalDatabaseId + process.env['JWT_PUBLIC_KEY'] = authConfig.authPublicKey + process.env['APP_DEF_ID'] = authConfig.kid } -const appendRoleToRequest = (role: string) => (dataRaw: string) => { +export const appendRoleToRequest = (role: string) => (dataRaw: string) => { const data = JSON.parse( dataRaw ) - return JSON.stringify({ ...data, ...{ requestContext: { ...data.requestContext, role } } }) + return JSON.stringify({ request: data, metadata: { requestContext: { ...data.requestContext, role } } }) } -const appendJWTHeaderToRequest = (dataRaw: string, headers: AxiosRequestHeaders) => { - headers['Authorization'] = createJwtHeader() +export const signTokenWith = (privateKey: string, appId: string, options = { algorithm: 'RS256' }) => (dataRaw: string, headers: AxiosRequestHeaders) => { + headers['Content-Type'] = 'text/plain' const data = JSON.parse( dataRaw ) - return JSON.stringify({ ...data } ) + const payload = { + data, + iss: TOKEN_ISSUER, + aud: appId, + } + const signedData = jwt.sign(payload, privateKey, options as jwt.SignOptions) + return signedData } -const TOKEN_ISSUER = 'wix-data.wix.com' - -const createJwtHeader = () => { - const token = jwt.sign({ iss: TOKEN_ISSUER, siteId: allowedMetasite, aud: externalDatabaseId }, authConfig.authPrivateKey, { algorithm: 'ES256', keyid: authConfig.kid }) - return `Bearer ${token}` -} export const authAdmin = { transformRequest: axios.defaults .transformRequest - .concat( appendJWTHeaderToRequest, appendRoleToRequest('BACKEND_CODE') ) } + .concat(appendRoleToRequest('BACKEND_CODE'), signTokenWith(authConfig.authPrivateKey, authConfig.kid)) } export const authOwner = { transformRequest: axios.defaults .transformRequest - .concat( appendJWTHeaderToRequest, appendRoleToRequest('OWNER' ) ) } + .concat(appendRoleToRequest('OWNER'), signTokenWith(authConfig.authPrivateKey, authConfig.kid)) } export const authVisitor = { transformRequest: axios.defaults .transformRequest - .concat( appendJWTHeaderToRequest, appendRoleToRequest('VISITOR' ) ) } + .concat(appendRoleToRequest('VISITOR'), signTokenWith(authConfig.authPrivateKey, authConfig.kid)) } export const authOwnerWithoutJwt = { transformRequest: axios.defaults .transformRequest - .concat( appendRoleToRequest('OWNER' ) ) } + .concat( appendRoleToRequest('OWNER') ) } + +export const authOwnerWithWrongJwtPublicKey = { transformRequest: axios.defaults + .transformRequest + .concat( appendRoleToRequest('OWNER'), signTokenWith(authConfig.otherAuthPrivateKey, authConfig.kid) ) } + + +export const authOwnerWithWrongAppId= { transformRequest: axios.defaults + .transformRequest + .concat( appendRoleToRequest('OWNER'), signTokenWith(authConfig.authPrivateKey, 'wrong-app-id') ) } + + export const errorResponseWith = (status: any, message: string) => ({ response: { data: { description: expect.stringContaining(message) }, status } }) diff --git a/libs/velo-external-db-core/src/index.ts b/libs/velo-external-db-core/src/index.ts index 64ee6ea25..42d2c1cd1 100644 --- a/libs/velo-external-db-core/src/index.ts +++ b/libs/velo-external-db-core/src/index.ts @@ -75,4 +75,5 @@ export * as dataSpi from './spi-model/data_source' export * as collectionSpi from './spi-model/collection' export * as schemaUtils from '../src/utils/schema_utils' export * as convertersUtils from './converters/utils' +export { config } from './roles-config.json' export { DataService, SchemaService, OperationService, CacheableSchemaInformation, FilterTransformer, AggregationTransformer, QueryValidator, SchemaAwareDataService, ItemTransformer, Hooks, ServiceContext, CollectionCapability, decodeBase64 } diff --git a/libs/velo-external-db-core/src/router.ts b/libs/velo-external-db-core/src/router.ts index 2e9b5da96..ff22d1962 100644 --- a/libs/velo-external-db-core/src/router.ts +++ b/libs/velo-external-db-core/src/router.ts @@ -21,19 +21,19 @@ import AggregationTransformer from './converters/aggregation_transformer' import { RoleAuthorizationService } from '@wix-velo/external-db-security' import { DataHooks, Hooks, RequestContext, SchemaHooks, ServiceContext } from './types' import { ConfigValidator } from '@wix-velo/external-db-config' -import { JwtAuthenticator } from './web/jwt-auth-middleware' import * as dataSource from './spi-model/data_source' import * as schemaSource from './spi-model/collection' import * as capabilities from './spi-model/capabilities' -import { WixDataFacade } from './web/wix_data_facade' +import { JWTVerifier } from './web/jwt-verifier' +import { JWTVerifierDecoderMiddleware } from './web/jwt-verifier-decoder-middleware' const { query: Query, count: Count, aggregate: Aggregate, insert: Insert, update: Update, remove: Remove, truncate: Truncate } = DataOperation const { Get, Create, Update: UpdateSchema, Delete } = CollectionOperationSPI -let schemaService: SchemaService, operationService: OperationService, externalDbConfigClient: ConfigValidator, schemaAwareDataService: SchemaAwareDataService, cfg: { allowedMetasites: string, type?: any; vendor?: any, wixDataBaseUrl: string, hideAppInfo?: boolean, jwtPublicKey?: string, appDefId?: string }, filterTransformer: FilterTransformer, aggregationTransformer: AggregationTransformer, dataHooks: DataHooks, schemaHooks: SchemaHooks //roleAuthorizationService: RoleAuthorizationService, +let schemaService: SchemaService, operationService: OperationService, externalDbConfigClient: ConfigValidator, schemaAwareDataService: SchemaAwareDataService, cfg: { allowedMetasites: string, type?: any; vendor?: any, wixDataBaseUrl: string, hideAppInfo?: boolean, jwtPublicKey: string, appDefId: string }, filterTransformer: FilterTransformer, aggregationTransformer: AggregationTransformer, dataHooks: DataHooks, schemaHooks: SchemaHooks //roleAuthorizationService: RoleAuthorizationService, export const initServices = (_schemaAwareDataService: SchemaAwareDataService, _schemaService: SchemaService, _operationService: OperationService, - _externalDbConfigClient: ConfigValidator, _cfg: { allowedMetasites: string, type?: string, vendor?: string, wixDataBaseUrl: string, hideAppInfo?: boolean, jwtPublicKey?: string, appDefId?: string }, + _externalDbConfigClient: ConfigValidator, _cfg: { allowedMetasites: string, type?: string, vendor?: string, wixDataBaseUrl: string, hideAppInfo?: boolean, jwtPublicKey: string, appDefId: string }, _filterTransformer: FilterTransformer, _aggregationTransformer: AggregationTransformer, _roleAuthorizationService: RoleAuthorizationService, _hooks: Hooks) => { schemaService = _schemaService @@ -83,12 +83,12 @@ const executeHook = async(hooks: DataHooks | SchemaHooks, _actionName: string, p export const createRouter = () => { const router = express.Router() - router.use(express.json()) + router.use(express.text()) router.use(compression()) router.use('/assets', express.static(path.join(__dirname, 'assets'))) - // TODO: jwt auth will be fixed in the following PR - const jwtAuthenticator = new JwtAuthenticator('', cfg.allowedMetasites, new WixDataFacade(cfg.wixDataBaseUrl)) - router.use(unless(['/', '/info', '/capabilities', '/favicon.ico', '/provision', '/connectionStatus'], jwtAuthenticator.authorizeJwt())) + const jwtVerifier = new JWTVerifier(cfg.jwtPublicKey, cfg.appDefId) + const jwtVerifierDecoderMiddleware = new JWTVerifierDecoderMiddleware(jwtVerifier) + router.use(unless(['/', '/info', '/capabilities', '/favicon.ico', '/provision', '/connectionStatus'], jwtVerifierDecoderMiddleware.verifyAndDecodeMiddleware())) config.forEach(({ pathPrefix, roles }) => router.use(includes([pathPrefix], authRoleMiddleware({ roles })))) diff --git a/libs/velo-external-db-core/src/spi-model/errors.ts b/libs/velo-external-db-core/src/spi-model/errors.ts index 4e4f4ddb7..5d0bfde25 100644 --- a/libs/velo-external-db-core/src/spi-model/errors.ts +++ b/libs/velo-external-db-core/src/spi-model/errors.ts @@ -286,6 +286,12 @@ export class ErrorMessage { } as InvalidPropertyDetails } as ErrorMessage, HttpStatusCode.INVALID_ARGUMENT) } + static unauthorized(description?: string) { + return HttpError.create({ + code: ApiErrors.WDE0027, + description + } as ErrorMessage, HttpStatusCode.UNAUTHENTICATED) + } } export interface HttpError { diff --git a/libs/velo-external-db-core/src/types.ts b/libs/velo-external-db-core/src/types.ts index 38da076bb..adcd13d52 100644 --- a/libs/velo-external-db-core/src/types.ts +++ b/libs/velo-external-db-core/src/types.ts @@ -89,8 +89,8 @@ export interface ExternalDbRouterConfig { commonExtended?: boolean hideAppInfo?: boolean wixDataBaseUrl: string - jwtPublicKey?: string - appDefId?: string + jwtPublicKey: string + appDefId: string } export type Hooks = { diff --git a/libs/velo-external-db-core/src/web/domain-to-spi-error-translator.ts b/libs/velo-external-db-core/src/web/domain-to-spi-error-translator.ts index e35b7d2bc..60ea923e1 100644 --- a/libs/velo-external-db-core/src/web/domain-to-spi-error-translator.ts +++ b/libs/velo-external-db-core/src/web/domain-to-spi-error-translator.ts @@ -26,6 +26,10 @@ export const domainToSpiErrorTranslator = (err: any) => { case domainErrors.ItemDoesNotExists: const itemDoesNotExists: domainErrors.ItemDoesNotExists = err return ErrorMessage.itemNotFound(itemDoesNotExists.itemId, itemDoesNotExists.collectionName, itemDoesNotExists.message) + + case domainErrors.UnauthorizedError: + const unauthorizedError: domainErrors.UnauthorizedError = err + return ErrorMessage.unauthorized(unauthorizedError.message) default: return ErrorMessage.unknownError(err.message, err.status) diff --git a/libs/velo-external-db-core/src/web/middleware-support.ts b/libs/velo-external-db-core/src/web/middleware-support.ts index b70756a0b..3371bf4a0 100644 --- a/libs/velo-external-db-core/src/web/middleware-support.ts +++ b/libs/velo-external-db-core/src/web/middleware-support.ts @@ -3,14 +3,12 @@ import { NextFunction, Request, Response } from 'express' import { has, get } from 'nested-property' const { UnauthorizedError } = errors -export const unless = function(path: string | any[], _middleware: any) { +export const unless = function(path: string | any[], middleware: any) { return function(req: Request, res: Response, next: NextFunction) { if (path.includes(req.path)) { return next() } else { - // TODO: will be replaced with jwt verification in the following PR - //return middleware(req, res, next) - return next() + return middleware(req, res, next) } } }