Skip to content

Commit

Permalink
feat(backend,frontend,mock-ase): use hmac signature to secure admin api
Browse files Browse the repository at this point in the history
  • Loading branch information
njlie committed Apr 3, 2024
1 parent 5c0ee4b commit 61692d1
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 5 deletions.
5 changes: 5 additions & 0 deletions localenv/cloud-nine-wallet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ services:
AUTH_SERVER_DOMAIN: ${CLOUD_NINE_AUTH_SERVER_DOMAIN:-http://localhost:3006}
TESTNET_AUTOPEER_URL: ${TESTNET_AUTOPEER_URL}
GRAPHQL_URL: http://cloud-nine-wallet-backend:3001/graphql
SIGNATURE_VERSION: 1
SIGNATURE_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
volumes:
- ../cloud-nine-wallet/seed.yml:/workspace/seed.yml
- ../cloud-nine-wallet/private-key.pem:/workspace/private-key.pem
Expand Down Expand Up @@ -55,6 +57,7 @@ services:
AUTH_SERVER_INTROSPECTION_URL: http://cloud-nine-wallet-auth:3007
ILP_ADDRESS: ${ILP_ADDRESS:-test.cloud-nine-wallet}
STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU=
API_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
OPEN_PAYMENTS_URL: ${CLOUD_NINE_OPEN_PAYMENTS_URL:-http://cloud-nine-wallet-backend}
WEBHOOK_URL: http://cloud-nine-wallet/webhooks
EXCHANGE_RATES_URL: http://cloud-nine-wallet/rates
Expand Down Expand Up @@ -118,6 +121,8 @@ services:
GRAPHQL_URL: http://cloud-nine-wallet-backend:3001/graphql
OPEN_PAYMENTS_URL: https://cloud-nine-wallet-backend/
ENABLE_INSECURE_MESSAGE_COOKIE: true
SIGNATURE_VERSION: 1
SIGNATURE_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
depends_on:
- cloud-nine-backend

Expand Down
5 changes: 5 additions & 0 deletions localenv/happy-life-bank/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ services:
KEY_FILE: /workspace/private-key.pem
OPEN_PAYMENTS_URL: ${HAPPY_LIFE_BANK_OPEN_PAYMENTS_URL:-https://happy-life-bank-backend}
GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql
SIGNATURE_VERSION: 1
SIGNATURE_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
volumes:
- ../happy-life-bank/seed.yml:/workspace/seed.yml
- ../happy-life-bank/private-key.pem:/workspace/private-key.pem
Expand Down Expand Up @@ -48,6 +50,7 @@ services:
AUTH_SERVER_INTROSPECTION_URL: http://happy-life-bank-auth:3007
ILP_ADDRESS: test.happy-life-bank
STREAM_SECRET: BjPXtnd00G2mRQwP/8ZpwyZASOch5sUXT5o0iR5b5wU=
API_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
WEBHOOK_URL: http://happy-life-bank/webhooks
OPEN_PAYMENTS_URL: ${HAPPY_LIFE_BANK_OPEN_PAYMENTS_URL:-http://happy-life-bank-backend}
EXCHANGE_RATES_URL: http://happy-life-bank/rates
Expand Down Expand Up @@ -86,6 +89,8 @@ services:
GRAPHQL_URL: http://happy-life-bank-backend:3001/graphql
OPEN_PAYMENTS_URL: https://happy-life-bank-backend/
ENABLE_INSECURE_MESSAGE_COOKIE: true
SIGNATURE_VERSION: 1
SIGNATURE_SECRET: iyIgCprjb9uL8wFckR+pLEkJWMB7FJhgkvqhTQR/964=
depends_on:
- cloud-nine-admin
- happy-life-backend
25 changes: 23 additions & 2 deletions localenv/mock-account-servicing-entity/app/lib/apolloClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { createHmac } from 'crypto'

import { canonicalize } from 'json-canonicalize'
import { print } from 'graphql/language/printer'
import type { NormalizedCacheObject } from '@apollo/client'
import {
createHttpLink,
Expand Down Expand Up @@ -29,10 +33,27 @@ const errorLink = onError(({ graphQLErrors }) => {
}
})

const authLink = setContext((_, { headers }) => {
const authLink = setContext((request, { headers }) => {
if (!process.env.SIGNATURE_SECRET || !process.env.SIGNATURE_VERSION)
return { headers }
const timestamp = Math.round(new Date().getTime() / 1000)
const version = process.env.SIGNATURE_VERSION

const { query, variables, operationName } = request
const formattedRequest = {
variables,
operationName,
query: print(query)
}

const payload = `${timestamp}.${canonicalize(formattedRequest)}`
const hmac = createHmac('sha256', process.env.SIGNATURE_SECRET)
hmac.update(payload)
const digest = hmac.digest('hex')
return {
headers: {
...headers
...headers,
signature: `t=${timestamp}, v${version}=${digest}`
}
}
})
Expand Down
1 change: 1 addition & 0 deletions localenv/mock-account-servicing-entity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@types/uuid": "^9.0.8",
"axios": "^1.6.8",
"graphql": "^16.8.1",
"json-canonicalize": "^1.0.6",
"mock-account-service-lib": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ import { PaymentMethodHandlerService } from './payment-method/handler/service'
import { IlpPaymentService } from './payment-method/ilp/service'
import { TelemetryService } from './telemetry/service'
import { ApolloArmor } from '@escape.tech/graphql-armor'
import { verifyApiSignature } from './shared/utils'

export interface AppContextData {
logger: Logger
container: AppContainer
Expand Down Expand Up @@ -348,6 +350,7 @@ export class App {

await this.apolloServer.start()

koa.use(cors())
koa.use(bodyParser())

koa.use(
Expand All @@ -366,6 +369,17 @@ export class App {
}
)

koa.use(async (ctx, next: Koa.Next): Promise<void> => {
this.logger.info(
{ requestBody: ctx.request.body, headers: ctx.request.headers },
'body to be hashed, headers'
)
if (!verifyApiSignature(ctx, this.config) && this.config.apiSecret) {
ctx.throw(401, 'Unauthorized')
}
return next()
})

koa.use(
koaMiddleware(this.apolloServer, {
context: async (): Promise<ApolloContext> => {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ export const Config = {
signatureSecret: process.env.SIGNATURE_SECRET, // optional
signatureVersion: envInt('SIGNATURE_VERSION', 1),

apiSecret: process.env.API_SECRET, // optional

keyId: envString('KEY_ID', 'rafiki'),
privateKey: loadOrGenerateKey(envString('PRIVATE_KEY_FILE', '')),

Expand Down
23 changes: 23 additions & 0 deletions packages/backend/src/shared/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { validate, version } from 'uuid'
import { URL, type URL as URLType } from 'url'
import { Context } from 'koa'
import { createHmac } from 'crypto'
import { canonicalize } from 'json-canonicalize'
import { IAppConfig } from '../config/app'

export function validateId(id: string): boolean {
return validate(id) && version(id) === 4
Expand Down Expand Up @@ -91,3 +95,22 @@ export async function poll<T>(args: PollArgs<T>): Promise<T> {
// eslint-disable-next-line no-constant-condition
} while (true)
}

export function verifyApiSignature(ctx: Context, config: IAppConfig): boolean {
const { headers, body } = ctx.request
const signature = headers['signature']
if (!signature) {
return false
}

const signatureParts = (signature as string)?.split(', ')
const timestamp = signatureParts[0].split('=')[1]
const signatureDigest = signatureParts[1].split('=')[1]

const payload = `${timestamp}.${canonicalize(body)}`
const hmac = createHmac('sha256', config.apiSecret as string)
hmac.update(payload)
const digest = hmac.digest('hex')

return digest === signatureDigest
}
46 changes: 43 additions & 3 deletions packages/frontend/app/lib/apollo.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { createHmac } from 'crypto'

import {
ApolloClient,
ApolloLink,
InMemoryCache,
createHttpLink
} from '@apollo/client'
import type { NormalizedCacheObject } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import { canonicalize } from 'json-canonicalize'
import { print } from 'graphql/language/printer'

/* eslint-disable no-var */
declare global {
Expand All @@ -16,18 +26,48 @@ BigInt.prototype.toJSON = function (this: bigint) {
return this.toString()
}

const authLink = setContext((request, { headers }) => {
if (!process.env.SIGNATURE_SECRET || !process.env.SIGNATURE_VERSION)
return { headers }
const timestamp = Math.round(new Date().getTime() / 1000)
const version = process.env.SIGNATURE_VERSION

const { query, variables, operationName } = request
const formattedRequest = {
variables,
operationName,
query: print(query)
}

const payload = `${timestamp}.${canonicalize(formattedRequest)}`
const hmac = createHmac('sha256', process.env.SIGNATURE_SECRET)
hmac.update(payload)
const digest = hmac.digest('hex')

return {
headers: {
...headers,
signature: `t=${timestamp}, v${version}=${digest}`
}
}
})

const httpLink = createHttpLink({
uri: process.env.GRAPHQL_URL ?? 'http://localhost:3001/graphql'
})

if (!global.__apolloClient) {
global.__apolloClient = new ApolloClient({
cache: new InMemoryCache({}),
link: ApolloLink.from([authLink, httpLink]),
defaultOptions: {
query: {
fetchPolicy: 'no-cache'
},
mutate: {
fetchPolicy: 'no-cache'
}
},
uri: process.env.GRAPHQL_URL ?? 'http://localhost:3001/graphql'
}
})
}
const apolloClient = global.__apolloClient
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"graphql": "^16.8.1",
"ilp-packet": "3.1.4-alpha.2",
"isbot": "^5.1.2",
"json-canonicalize": "^1.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"uuid": "^9.0.1",
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 61692d1

Please sign in to comment.