From 898f51cac1840d9b6b7234bb61ba886c375a0f7f Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:26:02 -0400 Subject: [PATCH] feat: add x-auth-token req validation --- infra/cloud_function.tf | 1 + infra/local-dotenv-file.tf | 2 ++ infra/secret-manager.tf | 15 +++++++++++++ infra/variables.tf | 19 ++++++++++++++++ src/config.ts | 3 +++ src/index.ts | 18 ++++++++++----- src/validate-request-origin.ts | 41 ++++++++-------------------------- test-deployed-function.sh | 2 ++ 8 files changed, 64 insertions(+), 37 deletions(-) diff --git a/infra/cloud_function.tf b/infra/cloud_function.tf index 6942b65..12534ae 100644 --- a/infra/cloud_function.tf +++ b/infra/cloud_function.tf @@ -35,6 +35,7 @@ resource "google_cloudfunctions2_function" "watchdog_notifications" { TELEGRAM_BOT_TOKEN_SECRET_ID = google_secret_manager_secret.telegram_bot_token.secret_id TELEGRAM_CHAT_ID = var.telegram_chat_id QUICKNODE_SECURITY_TOKEN_SECRET_ID = google_secret_manager_secret.quicknode_security_token.secret_id + X_AUTH_TOKEN_SECRET_ID = google_secret_manager_secret.x_auth_token.secret_id # Logs execution ID for easier debugging => https://cloud.google.com/functions/docs/monitoring/logging#viewing_runtime_logs LOG_EXECUTION_ID = true diff --git a/infra/local-dotenv-file.tf b/infra/local-dotenv-file.tf index edde379..a68213a 100644 --- a/infra/local-dotenv-file.tf +++ b/infra/local-dotenv-file.tf @@ -5,5 +5,7 @@ resource "local_file" "env_file" { DISCORD_WEBHOOK_URL_SECRET_ID=${var.discord_webhook_url_secret_id} TELEGRAM_BOT_TOKEN_SECRET_ID=${var.telegram_bot_token_secret_id} TELEGRAM_CHAT_ID=${var.telegram_chat_id} + QUICKNODE_SECURITY_TOKEN_SECRET_ID=${var.quicknode_security_token_secret_id} + X_AUTH_TOKEN_SECRET_ID=${var.x_auth_token_secret_id} EOT } diff --git a/infra/secret-manager.tf b/infra/secret-manager.tf index 9f476c2..add1cf3 100644 --- a/infra/secret-manager.tf +++ b/infra/secret-manager.tf @@ -30,6 +30,21 @@ resource "google_secret_manager_secret_version" "telegram_bot_token" { secret_data = var.telegram_bot_token } +# Creates a new secret for the x-auth-token header, which is used to authenticate requests of origin other than Quicknode. +resource "google_secret_manager_secret" "x_auth_token" { + project = module.bootstrap.seed_project_id + secret_id = var.x_auth_token_secret_id + + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "x_auth_token" { + secret = google_secret_manager_secret.x_auth_token.id + secret_data = var.x_auth_token +} + # Creates a new secret for the Quicknode Security Token which is used to verify that requests to the Cloud Function are coming from Quicknode. resource "google_secret_manager_secret" "quicknode_security_token" { project = module.bootstrap.seed_project_id diff --git a/infra/variables.tf b/infra/variables.tf index 410067f..49ad196 100644 --- a/infra/variables.tf +++ b/infra/variables.tf @@ -70,6 +70,25 @@ variable "quicknode_api_key" { sensitive = true } +# You can look this up via: +# `gcloud secrets list` +variable "quicknode_security_token_secret_id" { + type = string + default = "quicknode-security-token" +} + +# You can look this up via: +# `gcloud secrets list` +variable "x_auth_token_secret_id" { + type = string + default = "x-auth-token" +} + +variable "x_auth_token" { + type = string + sensitive = true +} + variable "function_name" { type = string default = "watchdog-notifications" diff --git a/src/config.ts b/src/config.ts index 7544401..d6938d3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,6 +4,7 @@ export interface Env { GCP_PROJECT_ID: string; DISCORD_WEBHOOK_URL_SECRET_ID: string; QUICKNODE_SECURITY_TOKEN_SECRET_ID: string; + X_AUTH_TOKEN_SECRET_ID: string; TELEGRAM_BOT_TOKEN_SECRET_ID: string; TELEGRAM_CHAT_ID: string; } @@ -14,6 +15,7 @@ const schema: JSONSchemaType = { "GCP_PROJECT_ID", "DISCORD_WEBHOOK_URL_SECRET_ID", "QUICKNODE_SECURITY_TOKEN_SECRET_ID", + "X_AUTH_TOKEN_SECRET_ID", "TELEGRAM_BOT_TOKEN_SECRET_ID", "TELEGRAM_CHAT_ID", ], @@ -21,6 +23,7 @@ const schema: JSONSchemaType = { GCP_PROJECT_ID: { type: "string" }, DISCORD_WEBHOOK_URL_SECRET_ID: { type: "string" }, QUICKNODE_SECURITY_TOKEN_SECRET_ID: { type: "string" }, + X_AUTH_TOKEN_SECRET_ID: { type: "string" }, TELEGRAM_BOT_TOKEN_SECRET_ID: { type: "string" }, TELEGRAM_CHAT_ID: { type: "string" }, }, diff --git a/src/index.ts b/src/index.ts index 7070396..4074d1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,17 +10,25 @@ import { EventType } from "./types.js"; import parseTransactionReceipts from "./parse-transaction-receipts"; import sendDiscordNotification from "./send-discord-notification"; import sendTelegramNotification from "./send-telegram-notification"; -// import validateRequestOrigin from "./validate-request-origin"; +import { isFromQuicknode, hasAuthToken } from "./validate-request-origin"; export const watchdogNotifier: HttpFunction = async ( req: Request, res: Response, ) => { + const isProduction = process.env.NODE_ENV !== "development"; try { - // TODO: Activate this after we've verified it's working via SortedOracle events - // if (process.env.NODE_ENV !== "development") { - // await validateRequestOrigin(req); - // } + if (isProduction) { + const isAuthorized = + (await isFromQuicknode(req)) || (await hasAuthToken(req)); + + if (!isAuthorized) { + console.error("Origin validation failed for request."); + res.status(401).send("Unauthorized"); + return; + } + } + const parsedEvents = parseTransactionReceipts(req.body); for (const parsedEvent of parsedEvents) { diff --git a/src/validate-request-origin.ts b/src/validate-request-origin.ts index 9f093dc..2e5e380 100644 --- a/src/validate-request-origin.ts +++ b/src/validate-request-origin.ts @@ -3,7 +3,7 @@ import crypto from "crypto"; import config from "./config"; import getSecret from "./get-secret"; -export default async function validateRequestOrigin(req: Request) { +export async function isFromQuicknode(req: Request): Promise { const quicknodeSecurityToken = await getSecret( config.QUICKNODE_SECURITY_TOKEN_SECRET_ID, ); @@ -12,40 +12,17 @@ export default async function validateRequestOrigin(req: Request) { const contentHash = req.headers["x-qn-content-hash"]; const timestamp = req.headers["x-qn-timestamp"]; - if (!nonce || typeof nonce !== "string") { - console.error("No valid quicknode nonce found in request headers:", nonce); - return; - } - - if (!contentHash || typeof contentHash !== "string") { - console.error( - "No valid quicknode content hash found in request headers:", - contentHash, - ); - return; - } - - if (!timestamp || typeof timestamp !== "string") { - console.error( - "No valid quicknode timestamp found in request headers:", - contentHash, - ); - return; - } - const hmac = crypto.createHmac("sha256", quicknodeSecurityToken); hmac.update(`${nonce}${contentHash}${timestamp}`); const expectedSignature = hmac.digest("base64"); - if (givenSignature === expectedSignature) { - console.log( - "The signature given matches the expected signature and is valid.", - ); - return; - } else { - throw new Error( - "Signature validation failed for the request's Quicknode signature header", - ); - } + return givenSignature === expectedSignature; +} + +export async function hasAuthToken(req: Request): Promise { + const authToken = req.headers["x-auth-token"]; + const expectedAuthToken = await getSecret(config.X_AUTH_TOKEN_SECRET_ID); + + return authToken === expectedAuthToken; } diff --git a/test-deployed-function.sh b/test-deployed-function.sh index a159d5f..862eb69 100755 --- a/test-deployed-function.sh +++ b/test-deployed-function.sh @@ -6,7 +6,9 @@ set -u # Treat unset variables as an error when substituting # This only works if the function has been deployed and your `terraform` can access the state backend raw_function_url=$(terraform -chdir=infra output -json function_uri) function_url=$(echo "${raw_function_url}" | jq -r) +auth_token=$(gcloud secrets versions access latest --secret x-auth-token) curl "${function_url}" \ -H "Content-Type: application/json" \ + -H "X-AUTH-TOKEN: $auth_token" \ -d @src/proposal-created.fixture.json