Skip to content

Commit

Permalink
feat: add x-auth-token req validation
Browse files Browse the repository at this point in the history
  • Loading branch information
nvtaveras committed Jul 31, 2024
1 parent 8f4c47b commit 898f51c
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 37 deletions.
1 change: 1 addition & 0 deletions infra/cloud_function.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions infra/local-dotenv-file.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
15 changes: 15 additions & 0 deletions infra/secret-manager.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions infra/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -14,13 +15,15 @@ const schema: JSONSchemaType<Env> = {
"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",
],
properties: {
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" },
},
Expand Down
18 changes: 13 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
41 changes: 9 additions & 32 deletions src/validate-request-origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
const quicknodeSecurityToken = await getSecret(
config.QUICKNODE_SECURITY_TOKEN_SECRET_ID,
);
Expand All @@ -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<boolean> {
const authToken = req.headers["x-auth-token"];
const expectedAuthToken = await getSecret(config.X_AUTH_TOKEN_SECRET_ID);

return authToken === expectedAuthToken;
}
2 changes: 2 additions & 0 deletions test-deployed-function.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 898f51c

Please sign in to comment.