Skip to content

Commit

Permalink
fix: add tenant info to apollo context
Browse files Browse the repository at this point in the history
  • Loading branch information
njlie committed Dec 16, 2024
1 parent 6ae0cf6 commit 23b121d
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 41 deletions.
28 changes: 21 additions & 7 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ import { LocalPaymentService } from './payment-method/local/service'
import { GrantService } from './open_payments/grant/service'
import { AuthServerService } from './open_payments/authServer/service'
import { Tenant } from './tenants/model'
import { verifyTenantOrOperatorApiSignature } from './shared/utils'
import {
getTenantFromApiSignature,
TenantApiSignatureResult
} from './shared/utils'
export interface AppContextData {
logger: Logger
container: AppContainer
Expand Down Expand Up @@ -228,6 +231,11 @@ type ContextType<T> = T extends (

const WALLET_ADDRESS_PATH = '/:walletAddressPath+'

export interface TenantedApolloContext extends ApolloContext {
tenant?: Tenant
isOperator: boolean
}

export interface AppServices {
logger: Promise<Logger>
telemetry: Promise<TelemetryService>
Expand Down Expand Up @@ -397,19 +405,25 @@ export class App {
}
)

let tenantApiSignatureResult: TenantApiSignatureResult
if (this.config.env !== 'test') {
async (ctx: TenantedHttpSigContext, next: Koa.Next): Promise<void> => {
if (!(await verifyTenantOrOperatorApiSignature(ctx, this.config))) {
ctx.throw(401, 'Unauthorized')
koa.use(
async (ctx: TenantedHttpSigContext, next: Koa.Next): Promise<void> => {
const result = await getTenantFromApiSignature(ctx, this.config)
if (!result) {
ctx.throw(401, 'Unauthorized')
}
tenantApiSignatureResult = result
return next()
}
return next()
}
)
}

koa.use(
koaMiddleware(this.apolloServer, {
context: async (): Promise<ApolloContext> => {
context: async (): Promise<TenantedApolloContext> => {
return {
...tenantApiSignatureResult,
container: this.container,
logger: await this.container.use('logger')
}
Expand Down
40 changes: 17 additions & 23 deletions packages/backend/src/shared/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { IocContract } from '@adonisjs/fold'
import { Redis } from 'ioredis'
import { faker } from '@faker-js/faker'
import { v4 } from 'uuid'
import assert from 'assert'
import {
isValidHttpUrl,
poll,
requestWithTimeout,
sleep,
verifyTenantOrOperatorApiSignature
getTenantFromApiSignature
} from './utils'
import { AppServices, AppContext, TenantedHttpSigContext } from '../app'
import { TestContainer, createTestApp } from '../tests/app'
Expand Down Expand Up @@ -322,7 +323,7 @@ describe('utils', (): void => {
${false} | ${'tenanted non-operator'}
${true} | ${'tenanted operator'}
`(
'returns true if $description request has valid signature',
'returns if $description request has valid signature',
async ({ isOperator }): Promise<void> => {
const requestBody = { test: 'value' }

Expand Down Expand Up @@ -352,20 +353,19 @@ describe('utils', (): void => {
)
ctx.request.body = requestBody

const result = await verifyTenantOrOperatorApiSignature(ctx, config)
expect(result).toEqual(true)

expect(ctx.tenant).toEqual(isOperator ? operator : tenant)
const result = await getTenantFromApiSignature(ctx, config)
assert.ok(result)
expect(result.tenant).toEqual(isOperator ? operator : tenant)

if (isOperator) {
expect(ctx.isOperator).toEqual(true)
expect(result.isOperator).toEqual(true)
} else {
expect(ctx.isOperator).toEqual(false)
expect(result.isOperator).toEqual(false)
}
}
)

test("returns false when signature isn't signed with tenant secret", async (): Promise<void> => {
test("returns undefined when signature isn't signed with tenant secret", async (): Promise<void> => {
const requestBody = { test: 'value' }
const signature = generateApiSignature(
'wrongsecret',
Expand All @@ -386,13 +386,11 @@ describe('utils', (): void => {
)
ctx.request.body = requestBody

const result = await verifyTenantOrOperatorApiSignature(ctx, config)
expect(result).toEqual(false)
expect(ctx.tenant).toBeUndefined()
expect(ctx.isOperator).toEqual(false)
const result = await getTenantFromApiSignature(ctx, config)
expect(result).toBeUndefined
})

test('returns false if tenant id is not included', async (): Promise<void> => {
test('returns undefined if tenant id is not included', async (): Promise<void> => {
const requestBody = { test: 'value' }
const signature = generateApiSignature(
tenant.apiSecret,
Expand All @@ -413,13 +411,11 @@ describe('utils', (): void => {

ctx.request.body = requestBody

const result = await verifyTenantOrOperatorApiSignature(ctx, config)
expect(result).toEqual(false)
expect(ctx.tenant).toBeUndefined()
expect(ctx.isOperator).toEqual(false)
const result = await getTenantFromApiSignature(ctx, config)
expect(result).toBeUndefined()
})

test('returns false if tenant does not exist', async (): Promise<void> => {
test('returns undefined if tenant does not exist', async (): Promise<void> => {
const requestBody = { test: 'value' }
const signature = generateApiSignature(
tenant.apiSecret,
Expand All @@ -443,11 +439,9 @@ describe('utils', (): void => {

const tenantService = await deps.use('tenantService')
const getSpy = jest.spyOn(tenantService, 'get')
const result = await verifyTenantOrOperatorApiSignature(ctx, config)
expect(result).toEqual(false)
const result = await getTenantFromApiSignature(ctx, config)
expect(result).toBeUndefined()
expect(getSpy).toHaveBeenCalled()
expect(ctx.tenant).toBeUndefined()
expect(ctx.isOperator).toEqual(false)
})
})
})
26 changes: 15 additions & 11 deletions packages/backend/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createHmac } from 'crypto'
import { canonicalize } from 'json-canonicalize'
import { IAppConfig } from '../config/app'
import { AppContext, TenantedHttpSigContext } from '../app'
import { Tenant } from '../tenants/model'

export function validateId(id: string): boolean {
return validate(id) && version(id) === 4
Expand Down Expand Up @@ -172,35 +173,40 @@ async function canApiSignatureBeProcessed(
return true
}

export interface TenantApiSignatureResult {
tenant?: Tenant
isOperator: boolean
}

/*
Verifies http signatures by first attempting to replicate it with a secret
associated with a tenant id in the headers, then with the configured admin secret.
associated with a tenant id in the headers.
If a tenant secret can replicate the signature, the request is tenanted to that particular tenant.
If the environment admin secret matches the tenant's secret, then it is an operator request with elevated permissions.
If neither can replicate the signature then it is unauthorized.
*/
export async function verifyTenantOrOperatorApiSignature(
export async function getTenantFromApiSignature(
ctx: TenantedHttpSigContext,
config: IAppConfig
): Promise<boolean> {
): Promise<TenantApiSignatureResult | undefined> {
ctx.tenant = undefined
ctx.isOperator = false
const { headers } = ctx.request
const signature = headers['signature']
if (!signature) {
return false
return undefined
}

const tenantService = await ctx.container.use('tenantService')
const tenantId = headers['tenant-id']
const tenant = tenantId ? await tenantService.get(tenantId) : undefined

if (!tenant) return false
if (!tenant) return undefined

if (!(await canApiSignatureBeProcessed(signature, ctx, config))) return false
if (!(await canApiSignatureBeProcessed(signature, ctx, config)))
return undefined

// First, try validating with the tenant api secret
if (
tenant.apiSecret &&
verifyApiSignatureDigest(
Expand All @@ -210,12 +216,10 @@ export async function verifyTenantOrOperatorApiSignature(
tenant.apiSecret
)
) {
ctx.tenant = tenant
ctx.isOperator = tenant.apiSecret === config.adminApiSecret
return true
return { tenant, isOperator: tenant.apiSecret === config.adminApiSecret }
}

return false
return undefined
}

export async function verifyApiSignature(
Expand Down

0 comments on commit 23b121d

Please sign in to comment.