From 1c43cddd2feb308d1a8040a68a5eba71e0a0f535 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Mon, 16 Dec 2024 12:45:11 +0100 Subject: [PATCH] feat(integration): sign Admin API requests during integration tests (#3177) * fix(backend): await signature verification * test(integration): add signatures to apollo client requests * test(backend): sign GraphQL requests in test environment * Revert "test(backend): sign GraphQL requests in test environment" This reverts commit 0a128d1b0ebad8281a7e6401da3b25c628c5a033. * chore(backend): remove sig verification in test files --- packages/backend/src/app.ts | 4 +- pnpm-lock.yaml | 7 ++- test/integration/lib/apollo-client.ts | 50 +++++++++++++++++-- test/integration/lib/config.ts | 13 ++++- test/integration/lib/mock-ase.ts | 6 ++- test/integration/package.json | 2 + .../testenv/cloud-nine-wallet/.env | 2 + .../cloud-nine-wallet/docker-compose.yml | 1 + test/integration/testenv/happy-life-bank/.env | 2 + .../happy-life-bank/docker-compose.yml | 1 + 10 files changed, 79 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts index 3bbe8d2662..fe7b31f3c6 100644 --- a/packages/backend/src/app.ts +++ b/packages/backend/src/app.ts @@ -383,9 +383,9 @@ export class App { } ) - if (this.config.adminApiSecret) { + if (this.config.env !== 'test') { koa.use(async (ctx, next: Koa.Next): Promise => { - if (!verifyApiSignature(ctx, this.config)) { + if (!(await verifyApiSignature(ctx, this.config))) { ctx.throw(401, 'Unauthorized') } return next() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80beef9929..b0a46fd0a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -779,9 +779,15 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 + graphql: + specifier: ^16.8.1 + version: 16.8.1 hostile: specifier: ^1.4.0 version: 1.4.0 + json-canonicalize: + specifier: ^1.0.6 + version: 1.0.6 koa: specifier: ^2.15.3 version: 2.15.3 @@ -13193,7 +13199,6 @@ packages: /json-canonicalize@1.0.6: resolution: {integrity: sha512-kP2iYpOS5SZHYhIaR1t9oG80d4uTY3jPoaBj+nimy3njtJk8+sRsVatN8pyJRDRtk9Su3+6XqA2U8k0dByJBUQ==} - dev: false /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} diff --git a/test/integration/lib/apollo-client.ts b/test/integration/lib/apollo-client.ts index 6ef4647bd8..5cb5c06734 100644 --- a/test/integration/lib/apollo-client.ts +++ b/test/integration/lib/apollo-client.ts @@ -1,11 +1,55 @@ import type { NormalizedCacheObject } from '@apollo/client' -import { ApolloClient, InMemoryCache } from '@apollo/client' +import { + ApolloClient, + ApolloLink, + createHttpLink, + InMemoryCache +} from '@apollo/client' +import { createHmac } from 'crypto' +import { print } from 'graphql/language/printer' +import { canonicalize } from 'json-canonicalize' +import { setContext } from '@apollo/client/link/context' -export function createApolloClient( +interface CreateApolloClientArgs { graphqlUrl: string + signatureSecret: string + signatureVersion: string +} + +function createAuthLink(args: CreateApolloClientArgs) { + return setContext((request, { headers }) => { + const timestamp = Math.round(new Date().getTime() / 1000) + const version = args.signatureVersion + + const { query, variables, operationName } = request + const formattedRequest = { + variables, + operationName, + query: print(query) + } + + const payload = `${timestamp}.${canonicalize(formattedRequest)}` + const hmac = createHmac('sha256', args.signatureSecret) + hmac.update(payload) + const digest = hmac.digest('hex') + return { + headers: { + ...headers, + signature: `t=${timestamp}, v${version}=${digest}` + } + } + }) +} + +export function createApolloClient( + args: CreateApolloClientArgs ): ApolloClient { + const httpLink = createHttpLink({ + uri: args.graphqlUrl + }) + return new ApolloClient({ - uri: graphqlUrl, + link: ApolloLink.from([createAuthLink(args), httpLink]), cache: new InMemoryCache(), defaultOptions: { query: { diff --git a/test/integration/lib/config.ts b/test/integration/lib/config.ts index 86aab0b22d..723a36f87d 100644 --- a/test/integration/lib/config.ts +++ b/test/integration/lib/config.ts @@ -11,6 +11,8 @@ export type TestConfig = Config & { interactionServer: string walletAddressUrl: string keyId: string + signatureSecret: string + signatureVersion: string } type EnvConfig = { @@ -22,7 +24,10 @@ type EnvConfig = { GRAPHQL_URL: string KEY_ID: string IDP_SECRET: string + SIGNATURE_SECRET: string + SIGNATURE_VERSION: string } + const REQUIRED_KEYS: (keyof EnvConfig)[] = [ 'OPEN_PAYMENTS_URL', 'AUTH_SERVER_DOMAIN', @@ -31,7 +36,9 @@ const REQUIRED_KEYS: (keyof EnvConfig)[] = [ 'WALLET_ADDRESS_URL', 'GRAPHQL_URL', 'KEY_ID', - 'IDP_SECRET' + 'IDP_SECRET', + 'SIGNATURE_SECRET', + 'SIGNATURE_VERSION' ] const loadEnv = (filePath: string): EnvConfig => { @@ -69,7 +76,9 @@ const createConfig = (name: string): TestConfig => { walletAddressUrl: env.WALLET_ADDRESS_URL, graphqlUrl: env.GRAPHQL_URL, keyId: env.KEY_ID, - idpSecret: env.IDP_SECRET + idpSecret: env.IDP_SECRET, + signatureSecret: env.SIGNATURE_SECRET, + signatureVersion: env.SIGNATURE_VERSION } } diff --git a/test/integration/lib/mock-ase.ts b/test/integration/lib/mock-ase.ts index 4642907b4f..4b9d7f81c9 100644 --- a/test/integration/lib/mock-ase.ts +++ b/test/integration/lib/mock-ase.ts @@ -30,7 +30,11 @@ export class MockASE { // Use static MockASE.create instead. private constructor(config: TestConfig) { this.config = config - this.apolloClient = createApolloClient(config.graphqlUrl) + this.apolloClient = createApolloClient({ + graphqlUrl: config.graphqlUrl, + signatureSecret: config.signatureSecret, + signatureVersion: config.signatureVersion + }) this.adminClient = new AdminClient(this.apolloClient) this.accounts = new AccountProvider() this.integrationServer = new IntegrationServer( diff --git a/test/integration/package.json b/test/integration/package.json index f5393c8a62..ec68b9ef1b 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -22,7 +22,9 @@ "@types/koa-bodyparser": "^4.3.12", "@types/node": "^20.14.15", "dotenv": "^16.4.5", + "graphql": "^16.8.1", "hostile": "^1.4.0", + "json-canonicalize": "^1.0.6", "koa": "^2.15.3", "mock-account-service-lib": "workspace:*", "yaml": "^2.6.0" diff --git a/test/integration/testenv/cloud-nine-wallet/.env b/test/integration/testenv/cloud-nine-wallet/.env index f2cb1e1bbc..bf7a0b7fd0 100644 --- a/test/integration/testenv/cloud-nine-wallet/.env +++ b/test/integration/testenv/cloud-nine-wallet/.env @@ -5,5 +5,7 @@ INTEGRATION_SERVER_PORT=8888 WALLET_ADDRESS_URL=https://cloud-nine-wallet-test-backend:3100/.well-known/pay GRAPHQL_URL=http://cloud-nine-wallet-test-backend:3101/graphql IDP_SECRET=2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= +SIGNATURE_VERSION=1 +SIGNATURE_SECRET=iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= # matches pfry key id KEY_ID=keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5 \ No newline at end of file diff --git a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml index 02bdc3d156..18630e78eb 100644 --- a/test/integration/testenv/cloud-nine-wallet/docker-compose.yml +++ b/test/integration/testenv/cloud-nine-wallet/docker-compose.yml @@ -42,6 +42,7 @@ services: USE_TIGERBEETLE: false OPERATOR_TENANT_ID: 438fa74a-fa7d-4317-9ced-dde32ece1787 API_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= + API_SIGNATURE_VERSION: 1 volumes: - ../private-key.pem:/workspace/private-key.pem depends_on: diff --git a/test/integration/testenv/happy-life-bank/.env b/test/integration/testenv/happy-life-bank/.env index d5da571ecc..9ab63a2ed7 100644 --- a/test/integration/testenv/happy-life-bank/.env +++ b/test/integration/testenv/happy-life-bank/.env @@ -5,5 +5,7 @@ INTEGRATION_SERVER_PORT=8889 WALLET_ADDRESS_URL=https://happy-life-bank-test-backend:4100/accounts/pfry GRAPHQL_URL=http://happy-life-bank-test-backend:4101/graphql IDP_SECRET=2pEcn2kkCclbOHQiGNEwhJ0rucATZhrA807HTm2rNXE= +SIGNATURE_VERSION=1 +SIGNATURE_SECRET=iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= # matches pfry key id KEY_ID=keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5 \ No newline at end of file diff --git a/test/integration/testenv/happy-life-bank/docker-compose.yml b/test/integration/testenv/happy-life-bank/docker-compose.yml index 40fbc64263..a0a42586ba 100644 --- a/test/integration/testenv/happy-life-bank/docker-compose.yml +++ b/test/integration/testenv/happy-life-bank/docker-compose.yml @@ -40,6 +40,7 @@ services: USE_TIGERBEETLE: false OPERATOR_TENANT_ID: cf5fd7d3-1eb1-4041-8e43-ba45747e9e5d API_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964= + API_SIGNATURE_VERSION: 1 volumes: - ../private-key.pem:/workspace/private-key.pem depends_on: