Skip to content

Commit

Permalink
Add support for auth webhooks (#296)
Browse files Browse the repository at this point in the history
* Add support for auth webhooks

* update auth middleware

* Update auth webhook url
  • Loading branch information
adam-maj authored Nov 7, 2023
1 parent 157140c commit 3d1dd12
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 53 deletions.
1 change: 1 addition & 0 deletions src/schema/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum WebhooksEventTypes {
CANCELLED_TX = "cancelled_transaction",
ALL_TX = "all_transactions",
BACKEND_WALLET_BALANCE = "backend_wallet_balance",
AUTH = "auth",
}

export interface SanitizedWebHooksSchema {
Expand Down
140 changes: 96 additions & 44 deletions src/server/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import { getPermissions } from "../../db/permissions/getPermissions";
import { createToken } from "../../db/tokens/createToken";
import { getToken } from "../../db/tokens/getToken";
import { revokeToken } from "../../db/tokens/revokeToken";
import { WebhooksEventTypes } from "../../schema/webhooks";
import { getWebhookConfig } from "../../utils/cache/getWebhook";
import { env } from "../../utils/env";
import { logger } from "../../utils/logger";
import { Permission } from "../schemas/auth";
import { sendWebhookRequest } from "../utils/webhook";

export type TAuthData = never;
export type TAuthSession = { permissions: string };
Expand Down Expand Up @@ -199,66 +202,115 @@ export const withAuth = async (server: FastifyInstance) => {
// If the secret key is being used, treat the user as the auth wallet
const config = await getConfiguration();
const wallet = new LocalWallet();
await wallet.import({
encryptedJson: config.authWalletEncryptedJson,
password: env.THIRDWEB_API_SECRET_KEY,
});

try {
await wallet.import({
encryptedJson: config.authWalletEncryptedJson,
password: env.ENCRYPTION_PASSWORD,
});
} catch {
// If that fails, we try to load the wallet with the secret key
await wallet.import({
encryptedJson: config.authWalletEncryptedJson,
password: env.THIRDWEB_API_SECRET_KEY,
});

// And then update the auth wallet to use encryption password instead
const encryptedJson = await wallet.export({
strategy: "encryptedJson",
password: env.ENCRYPTION_PASSWORD,
});

logger.worker.info(
`[Encryption] Updating authWalletEncryptedJson to use ENCRYPTION_PASSWORD`,
);
await updateConfiguration({
authWalletEncryptedJson: encryptedJson,
});
}

req.user = {
address: await wallet.getAddress(),
session: {
permissions: Permission.Admin,
},
};

return;
}

// Otherwise, check for an authenticated user
const jwt = getJWT(req);
if (jwt) {
// 1. Check if the token is a valid engine JWT
const token = await getToken({ jwt });

// First, we ensure that the token hasn't been revoked
if (token?.revokedAt === null) {
// Then we perform our standard auth checks for the user
const user = await getUser(req);

// Ensure that the token user is an admin or owner
if (
(user && user?.session?.permissions === Permission.Owner) ||
user?.session?.permissions === Permission.Admin
) {
req.user = user;
return;
try {
const jwt = getJWT(req);
if (jwt) {
// 1. Check if the token is a valid engine JWT
const token = await getToken({ jwt });

// First, we ensure that the token hasn't been revoked
if (token?.revokedAt === null) {
// Then we perform our standard auth checks for the user
const user = await getUser(req);

// Ensure that the token user is an admin or owner
if (
(user && user?.session?.permissions === Permission.Owner) ||
user?.session?.permissions === Permission.Admin
) {
req.user = user;
return;
}
}
}

// 2. Otherwise, check if the token is a valid api-server JWT
const user =
(await authWithApiServer(jwt, "thirdweb.com")) ||
(await authWithApiServer(jwt, "thirdweb-preview.com"));

// If we have an api-server user, return it with the proper permissions
if (user) {
const res = await getPermissions({ walletAddress: user.address });

if (
res?.permissions === Permission.Owner ||
res?.permissions === Permission.Admin
) {
req.user = {
address: user.address,
session: {
permissions: res.permissions,
},
};
return;
// 2. Otherwise, check if the token is a valid api-server JWT
const user =
(await authWithApiServer(jwt, "thirdweb.com")) ||
(await authWithApiServer(jwt, "thirdweb-preview.com"));

// If we have an api-server user, return it with the proper permissions
if (user) {
const res = await getPermissions({ walletAddress: user.address });

if (
res?.permissions === Permission.Owner ||
res?.permissions === Permission.Admin
) {
req.user = {
address: user.address,
session: {
permissions: res.permissions,
},
};
return;
}
}
}
} catch {
// no-op
}

const authWebhooks = await getWebhookConfig(WebhooksEventTypes.AUTH);
if (authWebhooks) {
const authResponses = await Promise.all(
authWebhooks.map((webhook) =>
sendWebhookRequest(webhook, {
url: req.url,
method: req.method,
headers: req.headers,
params: req.params,
query: req.query,
cookies: req.cookies,
body: req.body,
}),
),
);

// If every auth webhook returns true, we allow the request
if (authResponses.every((ok) => !!ok)) {
return;
}
}
} catch {
// no-op
} catch (err: any) {
logger.server.error(`[Auth] ${err?.message || err}`);
}

// If we have no secret key or authenticated user, return 401
Expand Down
5 changes: 5 additions & 0 deletions src/server/routes/webhooks/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ BodySchema.examples = [
name: "Backend Wallet Balance Event",
eventType: WebhooksEventTypes.BACKEND_WALLET_BALANCE,
},
{
url: "http://localhost:3000/auth",
name: "Auth Check",
eventType: WebhooksEventTypes.AUTH,
},
];

const ReplySchema = Type.Object({
Expand Down
14 changes: 5 additions & 9 deletions src/server/utils/webhook.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Static } from "@sinclair/typebox";
import crypto from "crypto";
import { getConfiguration } from "../../db/configuration/getConfiguration";
import { getTxById } from "../../db/transactions/getTxById";
Expand All @@ -9,10 +8,7 @@ import {
} from "../../schema/webhooks";
import { getWebhookConfig } from "../../utils/cache/getWebhook";
import { logger } from "../../utils/logger";
import {
TransactionStatusEnum,
transactionResponseSchema,
} from "../schemas/transaction";
import { TransactionStatusEnum } from "../schemas/transaction";

let balanceNotificationLastSentAt = -1;

Expand All @@ -21,7 +17,7 @@ interface TxWebookParams {
}

export const generateSignature = (
body: Static<typeof transactionResponseSchema> | WalletBalanceWebhookSchema,
body: Record<string, any>,
timestamp: string,
secret: string,
): string => {
Expand All @@ -31,7 +27,7 @@ export const generateSignature = (

export const createWebhookRequestHeaders = async (
webhookConfig: SanitizedWebHooksSchema,
body: Static<typeof transactionResponseSchema> | WalletBalanceWebhookSchema,
body: Record<string, any>,
): Promise<HeadersInit> => {
const headers: {
Accept: string;
Expand All @@ -56,9 +52,9 @@ export const createWebhookRequestHeaders = async (
return headers;
};

const sendWebhookRequest = async (
export const sendWebhookRequest = async (
webhookConfig: SanitizedWebHooksSchema,
body: Static<typeof transactionResponseSchema> | WalletBalanceWebhookSchema,
body: Record<string, any>,
): Promise<boolean> => {
const headers = await createWebhookRequestHeaders(webhookConfig, body);
const response = await fetch(webhookConfig?.url, {
Expand Down

0 comments on commit 3d1dd12

Please sign in to comment.