Skip to content

Commit

Permalink
Enable JWT middleware (#468)
Browse files Browse the repository at this point in the history
* feat: new jwt middlware

* test: new auth test

* feat: unauthorized error
  • Loading branch information
MXPOL authored and Idokah committed Nov 22, 2023
1 parent 85cfe3a commit 5c4e694
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 71 deletions.
36 changes: 0 additions & 36 deletions apps/velo-external-db/test/e2e/app_auth.e2e._spec.ts

This file was deleted.

43 changes: 43 additions & 0 deletions apps/velo-external-db/test/e2e/app_auth.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -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())
})
50 changes: 29 additions & 21 deletions libs/external-db-testkit/src/lib/auth_test_support.ts
Original file line number Diff line number Diff line change
@@ -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 } })
1 change: 1 addition & 0 deletions libs/velo-external-db-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
16 changes: 8 additions & 8 deletions libs/velo-external-db-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }))))

Expand Down
6 changes: 6 additions & 0 deletions libs/velo-external-db-core/src/spi-model/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions libs/velo-external-db-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ export interface ExternalDbRouterConfig {
commonExtended?: boolean
hideAppInfo?: boolean
wixDataBaseUrl: string
jwtPublicKey?: string
appDefId?: string
jwtPublicKey: string
appDefId: string
}

export type Hooks = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions libs/velo-external-db-core/src/web/middleware-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down

0 comments on commit 5c4e694

Please sign in to comment.