Skip to content

Commit 0c08e41

Browse files
authored
feat(auth): start storing apiSecret in auth and add tenant signature middleware (#3696)
* feat(auth): add migration to seed operators api secret * feat(auth): propagate apiSecret during tenant creation * test(auth): update api secret tests & tableManager * chore(localenv): update ADMIN_API_SECRET in docker compose files * chore(bruno): update authApiSignatureSecret in requests * feat(auth): add getTenantFromApiSignature middleware * chore(auth): move tenant signature functions to separate file * feat(auth): update authed tenant middleware, and add tests * test(backend): update tenant test * chore(testenv): add ADMIN_API_SECRET to auth * feat(backend): call the auth service client if apiSecret has been updated * test(auth): update tenant signature test name * feat(auth): update tenant response from auth api to include the apiSecet * test(auth): add additional tests for tenant signature verification
1 parent b5c71d5 commit 0c08e41

File tree

36 files changed

+809
-274
lines changed

36 files changed

+809
-274
lines changed

bruno/collections/Rafiki/environments/Local Playground.bru

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ vars {
2626
backendApiSignatureVersion: 1
2727
backendApiSignatureSecret: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
2828
authApiSignatureVersion: 1
29-
authApiSignatureSecret: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=
29+
authApiSignatureSecret: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
3030
assetIdTigerBeetle: 1
3131
assetCode: USD
3232
assetScale: 2

localenv/cloud-nine-wallet/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ services:
121121
IDENTITY_SERVER_URL: http://localhost:3030/mock-idp/
122122
IDENTITY_SERVER_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE=
123123
COOKIE_KEY: 42397d1f371dd4b8b7d0308a689a57c882effd4ea909d792302542af47e2cd37
124-
ADMIN_API_SECRET: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=
124+
ADMIN_API_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
125125
OPERATOR_TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787
126126
SERVICE_API_PORT: 3011
127127
depends_on:

localenv/happy-life-bank/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ services:
113113
IDENTITY_SERVER_URL: http://localhost:3031/mock-idp/
114114
IDENTITY_SERVER_SECRET: 2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE=
115115
COOKIE_KEY: 42397d1f371dd4b8b7d0308a689a57c882effd4ea909d792302542af47e2cd37
116-
ADMIN_API_SECRET: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4=
116+
ADMIN_API_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
117117
OPERATOR_TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d
118118
SERVICE_API_PORT: 4011
119119
depends_on:

packages/auth/jest.env.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ process.env.IDENTITY_SERVER_SECRET =
55
process.env.AUTH_SERVER_URL = 'http://localhost:3006'
66
process.env.IDENTITY_SERVER_URL = 'http://localhost:3030/mock-idp/'
77
process.env.OPERATOR_TENANT_ID = 'cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d'
8+
process.env.ADMIN_API_SECRET = 'iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964='
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const OPERATOR_TENANT_ID = process.env['OPERATOR_TENANT_ID']
2+
const ADMIN_API_SECRET = process.env['ADMIN_API_SECRET']
3+
4+
/**
5+
* @param { import("knex").Knex } knex
6+
* @returns { Promise<void> }
7+
*/
8+
exports.up = function (knex) {
9+
if (!OPERATOR_TENANT_ID) {
10+
throw new Error(
11+
'Could not seed operator tenant API secret. Please configure OPERATOR_TENANT_ID environment variable'
12+
)
13+
}
14+
15+
if (!ADMIN_API_SECRET) {
16+
throw new Error(
17+
'Could not seed operator tenant API secret. Please configure ADMIN_API_SECRET environment variable'
18+
)
19+
}
20+
21+
return knex.schema
22+
.alterTable('tenants', function (table) {
23+
table.string('apiSecret')
24+
})
25+
.then(() => {
26+
return knex.raw(`
27+
UPDATE "tenants" SET "apiSecret" = '${ADMIN_API_SECRET}'
28+
WHERE "id" = '${OPERATOR_TENANT_ID}'
29+
`)
30+
})
31+
.then(() => {
32+
return knex.schema.alterTable('tenants', (table) => {
33+
table.string('apiSecret').notNullable().alter()
34+
})
35+
})
36+
}
37+
38+
/**
39+
* @param { import("knex").Knex } knex
40+
* @returns { Promise<void> }
41+
*/
42+
exports.down = function (knex) {
43+
return knex.schema.alterTable('tenants', function (table) {
44+
table.dropColumn('apiSecret')
45+
})
46+
}

packages/auth/src/access/service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('Access Service', (): void => {
3737
})
3838

3939
afterEach(async (): Promise<void> => {
40-
await truncateTables(appContainer.knex)
40+
await truncateTables(deps)
4141
})
4242

4343
afterAll(async (): Promise<void> => {

packages/auth/src/access/utils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe('Access utilities', (): void => {
6969
})
7070

7171
afterEach(async (): Promise<void> => {
72-
await truncateTables(appContainer.knex)
72+
await truncateTables(deps)
7373
})
7474

7575
afterAll(async (): Promise<void> => {

packages/auth/src/accessToken/routes.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('Access Token Routes', (): void => {
4747

4848
afterEach(async (): Promise<void> => {
4949
jest.useRealTimers()
50-
await truncateTables(appContainer.knex)
50+
await truncateTables(deps)
5151
})
5252

5353
afterAll(async (): Promise<void> => {

packages/auth/src/accessToken/service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('Access Token Service', (): void => {
3737

3838
afterEach(async (): Promise<void> => {
3939
jest.useRealTimers()
40-
await truncateTables(appContainer.knex)
40+
await truncateTables(deps)
4141
})
4242

4343
afterAll(async (): Promise<void> => {

packages/auth/src/app.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,13 @@ import { ApolloArmor } from '@escape.tech/graphql-armor'
5353
import { Redis } from 'ioredis'
5454
import { LoggingPlugin } from './graphql/plugin'
5555
import { gnapServerErrorMiddleware } from './shared/gnapErrors'
56-
import { verifyApiSignature } from './shared/utils'
56+
import {
57+
authenticatedTenantMiddleware,
58+
unauthenticatedTenantMiddleware
59+
} from './signature/tenant'
5760
import { TenantService } from './tenant/service'
61+
import { TenantRoutes } from './tenant/routes'
62+
import { Tenant } from './tenant/model'
5863

5964
export interface AppContextData extends DefaultContext {
6065
logger: Logger
@@ -90,6 +95,18 @@ export interface DatabaseCleanupRule {
9095
defaultExpirationOffsetDays: number
9196
}
9297

98+
export interface TenantedAppContext extends AppContext {
99+
tenantApiSignatureResult: {
100+
tenant: Tenant
101+
isOperator: boolean
102+
}
103+
}
104+
105+
export interface TenantedApolloContext extends ApolloContext {
106+
tenant: Tenant
107+
isOperator: boolean
108+
}
109+
93110
export interface AppServices {
94111
logger: Promise<Logger>
95112
knex: Promise<Knex>
@@ -104,6 +121,7 @@ export interface AppServices {
104121
interactionRoutes: Promise<InteractionRoutes>
105122
redis: Promise<Redis>
106123
tenantService: Promise<TenantService>
124+
tenantRoutes: Promise<TenantRoutes>
107125
}
108126

109127
export type AppContainer = IocContract<AppServices>
@@ -218,20 +236,23 @@ export class App {
218236
}
219237
)
220238

221-
if (this.config.adminApiSecret) {
222-
koa.use(async (ctx, next: Koa.Next): Promise<void> => {
223-
if (!(await verifyApiSignature(ctx, this.config))) {
224-
ctx.throw(401, 'Unauthorized')
225-
}
226-
227-
return next()
228-
})
229-
}
239+
// For tests, we still need to get the tenant in the middleware, but
240+
// we don't need to verify the signature nor prevent replay attacks
241+
koa.use(
242+
this.config.env !== 'test'
243+
? authenticatedTenantMiddleware
244+
: unauthenticatedTenantMiddleware
245+
)
230246

231247
koa.use(
232248
koaMiddleware(apolloServer, {
233-
context: async (): Promise<ApolloContext> => {
249+
context: async ({
250+
ctx
251+
}: {
252+
ctx: TenantedAppContext
253+
}): Promise<TenantedApolloContext> => {
234254
return {
255+
...ctx.tenantApiSignatureResult,
235256
container: this.container,
236257
logger: await this.container.use('logger')
237258
}

0 commit comments

Comments
 (0)