Skip to content

Commit

Permalink
use AWS KMS to sign and verify auth tokens (#117)
Browse files Browse the repository at this point in the history
* add auth package; initial integration to api app

* hook up kms; fix signing message

* add changeset; upgrade api deps

* update readmes

* fix vitest timezone

* add auth built step to api deployment

* add jwt parsing fallback; remove overrideUserAddress feature

* parse backend wallet from account signature header

* use backend wallet from middleware

* add todos

* upgrade dependencies

* clear auth error on valid jwt fallback
  • Loading branch information
alecananian authored Sep 17, 2024
1 parent 5b005cd commit cdb4721
Show file tree
Hide file tree
Showing 29 changed files with 14,322 additions and 12,328 deletions.
5 changes: 5 additions & 0 deletions .changeset/good-trainers-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@treasure-dev/auth": major
---

Initial release
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ apps/api/ @alecananian

# Packages

packages/auth/ @alecananian
packages/core/ @alecananian
packages/react/ @alecananian
packages/tailwind-config/ @alecananian
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ SDK for the Treasure ecosystem

## Packages

- [@treasure-dev/auth](./packages/auth)
- [@treasure-dev/tdk-core](./packages/core)
- [@treasure-dev/tdk-react](./packages/react)
- [@treasure-dev/tailwind-config](./packages/tailwind-config)
Expand Down
1 change: 1 addition & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
PORT=8080
AWS_PROFILE=dev
AWS_REGION=us-west-2
DATABASE_URL=postgresql://postgres:@localhost:5432/tdk_api?schema=public
DEFAULT_BACKEND_WALLET=
Expand Down
1 change: 1 addition & 0 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ COPY . .
RUN npm install --include=dev

# Build packages
RUN npm run build:auth
RUN npm run build:core

# Build application
Expand Down
1 change: 1 addition & 0 deletions apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Backend server powering the Treasure Development Kit

- [>= Node 20.11.0](https://nodejs.org/en)
- [PostgreSQL](https://www.postgresql.org) server
- AWS credentials set up for Secrets Manager and Key Management Service access

## Development

Expand Down
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@sentry/profiling-node": "^8.7.0",
"@sinclair/typebox": "^0.33.7",
"@thirdweb-dev/engine": "^0.0.15",
"@treasure-dev/auth": "*",
"@treasure-dev/tdk-core": "*",
"@wagmi/core": "^2.9.1",
"abitype": "^1.0.5",
Expand Down
20 changes: 14 additions & 6 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import { PrismaClient } from "@prisma/client";
import * as Sentry from "@sentry/node";
import { Engine } from "@thirdweb-dev/engine";
import { createAuth } from "@treasure-dev/auth";
import { TREASURE_RUBY_CHAIN_DEFINITION } from "@treasure-dev/tdk-core";
import { http, createConfig, fallback } from "@wagmi/core";
import {
Expand All @@ -14,7 +15,7 @@ import {
} from "@wagmi/core/chains";
import Fastify from "fastify";
import { createThirdwebClient } from "thirdweb";
import { createAuth } from "thirdweb/auth";
import { createAuth as createThirdwebAuth } from "thirdweb/auth";
import { privateKeyToAccount } from "thirdweb/wallets";
import { defineChain } from "viem";

Expand All @@ -37,6 +38,10 @@ const main = async () => {

const env = await getEnv();
const client = createThirdwebClient({ secretKey: env.THIRDWEB_SECRET_KEY });
const adminAccount = privateKeyToAccount({
client,
privateKey: env.THIRDWEB_AUTH_PRIVATE_KEY,
});
const ctx: TdkApiContext = {
env,
db: new PrismaClient({
Expand All @@ -48,12 +53,15 @@ const main = async () => {
}),
client,
auth: createAuth({
domain: env.THIRDWEB_AUTH_DOMAIN!,
kmsKey: env.TREASURE_AUTH_KMS_KEY,
issuer: env.THIRDWEB_AUTH_DOMAIN,
audience: adminAccount.address,
expirationTimeSeconds: 86_400, // 1 day
}),
thirdwebAuth: createThirdwebAuth({
domain: env.THIRDWEB_AUTH_DOMAIN,
client,
adminAccount: privateKeyToAccount({
client,
privateKey: env.THIRDWEB_AUTH_PRIVATE_KEY!,
}),
adminAccount,
login: {
uri: `https://${env.THIRDWEB_AUTH_DOMAIN}`,
},
Expand Down
90 changes: 76 additions & 14 deletions apps/api/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,99 @@
import * as Sentry from "@sentry/node";
import type { AddressString, UserContext } from "@treasure-dev/tdk-core";
import type { FastifyInstance } from "fastify";
import { hashMessage, isHex, recoverAddress } from "viem";

import type { TdkApiContext } from "../types";
import { verifyAuth } from "../utils/auth";
import { throwUnauthorizedError } from "../utils/error";

declare module "fastify" {
interface FastifyRequest {
userId: string | undefined;
userAddress: AddressString | undefined;
overrideUserAddress: AddressString | undefined;
backendWallet: AddressString;
authError: string | undefined;
}
}

export const withAuth = async (
app: FastifyInstance,
{ auth }: TdkApiContext,
{ auth, thirdwebAuth, env }: TdkApiContext,
) => {
// Parse JWT header and obtain user address in middleware
app.decorateRequest("userId", undefined);
app.decorateRequest("userAddress", undefined);
app.decorateRequest("overrideUserAddress", undefined);
app.decorateRequest(
"backendWallet",
env.DEFAULT_BACKEND_WALLET as AddressString,
);
app.decorateRequest("authError", undefined);
app.addHook("onRequest", async (req) => {
if (req.headers.authorization) {
const authResult = await verifyAuth(auth, req);
// Check for explicit setting of user address and recover backend wallet address from signature
if (req.headers["x-account-address"]) {
const accountAddress = req.headers["x-account-address"].toString();
const signature = req.headers["x-account-signature"]?.toString();
if (!isHex(accountAddress) || !isHex(signature)) {
throwUnauthorizedError("Invalid account address or signature");
}

const backendWallet = await recoverAddress({
hash: hashMessage(
JSON.stringify({
accountAddress,
}),
),
signature: signature as AddressString,
});
if (!isHex(backendWallet)) {
throwUnauthorizedError("Invalid backend wallet address");
}

req.userAddress = accountAddress as AddressString;
req.backendWallet = backendWallet as AddressString;
Sentry.setUser({ username: req.userAddress });
return;
}

// Fall back to backend wallet from params
const backendWallet =
req.params &&
typeof req.params === "object" &&
"backendWallet" in req.params
? req.params.backendWallet
: undefined;
// TODO: Remove default backend wallet when all partners upgrade to their own
req.backendWallet = isHex(backendWallet)
? backendWallet
: (env.DEFAULT_BACKEND_WALLET as AddressString);

// All other auth methods require an Authorization header
if (!req.headers.authorization) {
return;
}

// Check for user address via JWT header
try {
const decoded = await auth.verifyJWT<UserContext>(
req.headers.authorization.replace("Bearer ", ""),
);
req.userId = decoded.ctx.id;
req.userAddress = decoded.sub as AddressString;
Sentry.setUser({
id: req.userId,
username: req.userAddress,
});
return;
} catch (err) {
req.authError = err instanceof Error ? err.message : "Unknown error";
}

// Fall back to legacy Thirdweb auth
// TODO: remove fallback when all legacy JWTs expire
try {
const authResult = await thirdwebAuth.verifyJWT({
jwt: req.headers.authorization.replace("Bearer ", ""),
});
if (authResult.valid) {
req.authError = undefined;
req.userId = (authResult.parsedJWT.ctx as UserContext | undefined)?.id;
req.userAddress = authResult.parsedJWT.sub as AddressString;
Sentry.setUser({
Expand All @@ -36,13 +103,8 @@ export const withAuth = async (
} else {
req.authError = authResult.error;
}
}

req.overrideUserAddress = req.headers["x-account-address"]?.toString() as
| AddressString
| undefined;
if (req.overrideUserAddress) {
Sentry.setUser({ username: req.overrideUserAddress });
} catch (err) {
req.authError = err instanceof Error ? err.message : "Unknown error";
}
});
};
20 changes: 14 additions & 6 deletions apps/api/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ import { fetchEmbeddedWalletUser } from "../utils/embeddedWalletApi";
import { transformUserProfileResponseFields } from "../utils/user";

export const authRoutes =
({ env, auth, db, engine, wagmiConfig }: TdkApiContext): FastifyPluginAsync =>
({
env,
auth,
thirdwebAuth,
db,
engine,
wagmiConfig,
}: TdkApiContext): FastifyPluginAsync =>
async (app) => {
app.get<{
Querystring: ReadLoginPayloadQuerystring;
Expand All @@ -44,7 +51,7 @@ export const authRoutes =
},
},
async (req, reply) => {
const payload = await auth.generatePayload({
const payload = await thirdwebAuth.generatePayload({
address: req.query.address,
chainId: req.chainId,
});
Expand All @@ -65,7 +72,7 @@ export const authRoutes =
},
},
async (req, reply) => {
const verifiedPayload = await auth.verifyPayload(req.body);
const verifiedPayload = await thirdwebAuth.verifyPayload(req.body);
if (!verifiedPayload.valid) {
return reply
.code(400)
Expand Down Expand Up @@ -134,10 +141,11 @@ export const authRoutes =
smartAccountAddress: user.address,
};

// Add user data to JWT payload's context
const [authToken, allActiveSigners, profile] = await Promise.all([
auth.generateJWT({
payload,
auth.generateJWT(user.address, {
issuer: payload.domain,
issuedAt: new Date(payload.issued_at),
expiresAt: new Date(payload.expiration_time),
context: userContext,
}),
getAllActiveSigners({
Expand Down
9 changes: 3 additions & 6 deletions apps/api/src/routes/harvesters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ export const harvestersRoutes =
const {
chainId,
params: { id },
userAddress: authUserAddress,
overrideUserAddress,
userAddress,
} = req;

const harvesterAddress = id as AddressString;
Expand All @@ -69,7 +68,6 @@ export const harvestersRoutes =
});
}

const userAddress = overrideUserAddress ?? authUserAddress;
const harvesterUserInfo = userAddress
? await getHarvesterUserInfo({
chainId,
Expand Down Expand Up @@ -108,15 +106,14 @@ export const harvestersRoutes =
const {
chainId,
params: { id },
userAddress: authUserAddress,
overrideUserAddress,
userAddress,
} = req;

const harvesterCorruptionRemovalInfo =
await fetchHarvesterCorruptionRemovalInfo({
chainId,
harvesterAddress: id,
userAddress: overrideUserAddress ?? authUserAddress,
userAddress,
inventoryApiUrl: env.TROVE_API_URL,
inventoryApiKey: env.TROVE_API_KEY,
wagmiConfig,
Expand Down
9 changes: 3 additions & 6 deletions apps/api/src/routes/magicswap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ export const magicswapRoutes =
nftsOut,
isExactOut,
slippage,
backendWallet = env.DEFAULT_BACKEND_WALLET,
simulateTransaction = env.ENGINE_TRANSACTION_SIMULATION_ENABLED,
} = body;

Expand Down Expand Up @@ -245,7 +244,7 @@ export const magicswapRoutes =
engine,
chainId,
contractAddress: swapArguments.address,
backendWallet,
backendWallet: req.backendWallet,
smartAccountAddress: userAddress,
abi: magicswapV2RouterAbi,
functionName: swapArguments.functionName,
Expand Down Expand Up @@ -303,7 +302,6 @@ export const magicswapRoutes =
amount1Min,
nfts0,
nfts1,
backendWallet = env.DEFAULT_BACKEND_WALLET,
simulateTransaction = env.ENGINE_TRANSACTION_SIMULATION_ENABLED,
} = body;

Expand Down Expand Up @@ -341,7 +339,7 @@ export const magicswapRoutes =
engine,
chainId,
contractAddress: addLiquidityArgs.address,
backendWallet,
backendWallet: req.backendWallet,
smartAccountAddress: userAddress,
abi: magicswapV2RouterAbi,
functionName: addLiquidityArgs.functionName,
Expand Down Expand Up @@ -399,7 +397,6 @@ export const magicswapRoutes =
nfts0,
nfts1,
swapLeftover = true,
backendWallet = env.DEFAULT_BACKEND_WALLET,
simulateTransaction = env.ENGINE_TRANSACTION_SIMULATION_ENABLED,
} = body;

Expand Down Expand Up @@ -437,7 +434,7 @@ export const magicswapRoutes =
engine,
chainId,
contractAddress: removeLiquidityArgs.address,
backendWallet,
backendWallet: req.backendWallet,
smartAccountAddress: userAddress,
abi: magicswapV2RouterAbi,
functionName: removeLiquidityArgs.functionName,
Expand Down
6 changes: 2 additions & 4 deletions apps/api/src/routes/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export const transactionsRoutes =
functionName,
args,
txOverrides,
backendWallet = env.DEFAULT_BACKEND_WALLET,
simulateTransaction = env.ENGINE_TRANSACTION_SIMULATION_ENABLED,
},
} = req;
Expand Down Expand Up @@ -109,7 +108,7 @@ export const transactionsRoutes =
const { result } = await engine.contract.write(
chainId.toString(),
address,
backendWallet,
req.backendWallet,
{
abi: transactionAbi,
functionName,
Expand Down Expand Up @@ -161,7 +160,6 @@ export const transactionsRoutes =
value = "0x00",
data,
txOverrides,
backendWallet = env.DEFAULT_BACKEND_WALLET,
simulateTransaction = env.ENGINE_TRANSACTION_SIMULATION_ENABLED,
},
} = req;
Expand All @@ -178,7 +176,7 @@ export const transactionsRoutes =
Sentry.setExtra("transaction", { to, value, data });
const { result } = await engine.backendWallet.sendTransaction(
chainId.toString(),
backendWallet,
req.backendWallet,
{
toAddress: to,
value: value,
Expand Down
Loading

0 comments on commit cdb4721

Please sign in to comment.