diff --git a/frontend/src/views/developer/CreateAPIKey.tsx b/frontend/src/views/developer/CreateAPIKey.tsx index 6d9249bc2..b9bb10312 100644 --- a/frontend/src/views/developer/CreateAPIKey.tsx +++ b/frontend/src/views/developer/CreateAPIKey.tsx @@ -18,7 +18,7 @@ import { useDevContext } from "./DevContext"; const CreateAPIKey = () => { const { env, mutate } = useDevContext(); - const axiosInstance = useAxiosInstance({env}); + const axiosInstance = useAxiosInstance({ env }); const [loading, setLoading] = useState(false); const [name, setName] = useState(""); @@ -62,7 +62,7 @@ const CreateAPIKey = () => { Create API Key - + Create API Key diff --git a/server/src/internal/api/apiRouter.ts b/server/src/internal/api/apiRouter.ts index 1c55b7447..6e92077f6 100644 --- a/server/src/internal/api/apiRouter.ts +++ b/server/src/internal/api/apiRouter.ts @@ -15,8 +15,14 @@ const apiRouter = Router(); apiRouter.use(apiAuthMiddleware); apiRouter.use(pricingMiddleware); - apiRouter.use(attachRouter); + +apiRouter.get("/auth", (req: any, res) => { + res.json({ + message: `Authenticated -- Hello ${req.minOrg?.slug}!`, + }); +}); + apiRouter.use("/customers", cusRouter); apiRouter.use("/products", productApiRouter); apiRouter.use("/features", featureApiRouter); diff --git a/server/src/internal/dev/ApiKeyService.ts b/server/src/internal/dev/ApiKeyService.ts index 9ed5b6108..9b77c24d8 100644 --- a/server/src/internal/dev/ApiKeyService.ts +++ b/server/src/internal/dev/ApiKeyService.ts @@ -32,4 +32,37 @@ export class ApiKeyService { return count; } + + static async getByHashedKey(sb: SupabaseClient, hashedKey: string) { + const { data, error } = await sb + .from("api_keys") + .select("*") + .eq("hashed_key", hashedKey) + .single(); + + if (error) { + if (error.code === "PGRST116") { + return null; + } + throw new Error("Failed to get API key"); + } + + return data; + } + + static async update({ + sb, + update, + keyId, + }: { + sb: SupabaseClient; + update: any; + keyId: string; + }) { + const { error } = await sb.from("api_keys").update(update).eq("id", keyId); + + if (error) { + throw new Error("Failed to update API key"); + } + } } diff --git a/server/src/internal/dev/api-keys/apiKeyUtils.ts b/server/src/internal/dev/api-keys/apiKeyUtils.ts new file mode 100644 index 000000000..b0c95ad25 --- /dev/null +++ b/server/src/internal/dev/api-keys/apiKeyUtils.ts @@ -0,0 +1,118 @@ +import { generateId } from "@/utils/genUtils.js"; +import { ApiKey, AppEnv } from "@autumn/shared"; +import { SupabaseClient } from "@supabase/supabase-js"; +import crypto from "crypto"; +import { ApiKeyService } from "../ApiKeyService.js"; + +function generateApiKey(length = 32, prefix = "") { + try { + // Define allowed characters (alphanumeric only) + const allowedChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const array = new Uint8Array(length); + crypto.getRandomValues(array); + + // Convert random bytes to alphanumeric string + const key = Array.from(array) + .map((byte) => allowedChars[byte % allowedChars.length]) + .join(""); + + return prefix ? `${prefix}_${key}` : key; + } catch (error) { + console.error("Failed to generate API key:", error); + throw new Error("Failed to generate secure API key"); + } +} + +const hashApiKey = (apiKey: string) => { + return crypto.createHash("sha256").update(apiKey).digest("hex"); +}; + +export const createKey = async ({ + sb, + env, + name, + orgId, + prefix, + meta, +}: { + sb: SupabaseClient; + env: AppEnv; + name: string; + orgId: string; + prefix: string; + meta: any; +}) => { + const apiKey = generateApiKey(42, prefix); + const hashedKey = hashApiKey(apiKey); + + const apiKeyData: ApiKey = { + id: generateId("key"), + org_id: orgId, + user_id: "", + name, + prefix: apiKey.substring(0, 14), + created_at: Date.now(), + env, + hashed_key: hashedKey, + meta, + }; + + await ApiKeyService.insert(sb, apiKeyData); + + return apiKey; +}; + +export const verifyKey = async ({ + sb, + key, +}: { + sb: SupabaseClient; + key: string; +}) => { + const hashedKey = hashApiKey(key); + const apiKey = await ApiKeyService.getByHashedKey(sb, hashedKey); + + if (!apiKey) { + return { + valid: false, + data: null, + }; + } + + return { + valid: true, + data: apiKey, + }; +}; + +export const migrateKey = async ({ + sb, + keyId, + meta, + apiKey, +}: { + sb: SupabaseClient; + keyId: string; + meta: any; + apiKey: string; +}) => { + try { + const hashedKey = hashApiKey(apiKey); + + await ApiKeyService.update({ + sb, + update: { + id: keyId, + hashed_key: hashedKey, + meta, + }, + keyId, + }); + + console.log(`MIGRATED KEY FOR ${keyId}, ${meta.org_slug}`); + } catch (error) { + console.log(`ERROR: FAILED TO MIGRATE KEY FOR ${keyId}, ${meta.org_slug}`); + console.log(error); + } +}; diff --git a/server/src/internal/dev/devRouter.ts b/server/src/internal/dev/devRouter.ts index 68933a2b1..8cceacc2e 100644 --- a/server/src/internal/dev/devRouter.ts +++ b/server/src/internal/dev/devRouter.ts @@ -1,9 +1,10 @@ -import { createKey, deleteKey, updateKey } from "@/external/unkeyUtils.js"; +import { deleteKey } from "@/external/unkeyUtils.js"; import { withOrgAuth } from "@/middleware/authMiddleware.js"; -import { ApiKey, AppEnv } from "@autumn/shared"; +import { AppEnv } from "@autumn/shared"; import { Router } from "express"; import { ApiKeyService } from "./ApiKeyService.js"; import { OrgService } from "../orgs/OrgService.js"; +import { createKey } from "./api-keys/apiKeyUtils.js"; export const devRouter = Router(); @@ -23,40 +24,23 @@ devRouter.post("/api_key", withOrgAuth, async (req: any, res) => { const { name } = req.body; // 1. Create API key - let prefix = "am_test"; + let prefix = "am_sk_test"; if (env === AppEnv.Live) { - prefix = "am_live"; + prefix = "am_sk_live"; } - const apiKey = await createKey({ + sb: req.sb, env, name, - ownerId: orgId, + orgId, prefix, meta: { org_slug: req.minOrg.slug, }, }); - if (!apiKey.result) { - console.error("Failed to create API key", apiKey); - res.status(500).json({ error: "Failed to create API key" }); - return; - } - - const apiKeyData: ApiKey = { - id: apiKey.result!.keyId, - org_id: orgId, - user_id: req.user.id, - name, - prefix: apiKey.result!.key.substring(0, 10), - created_at: Date.now(), - env, - }; - - await ApiKeyService.insert(req.sb, apiKeyData); res.status(200).json({ - api_key: apiKey.result!.key, + api_key: apiKey, }); }); @@ -69,8 +53,6 @@ devRouter.delete("/api_key/:id", withOrgAuth, async (req: any, res) => { res.status(404).json({ error: "API key not found" }); return; } - - await deleteKey(id); } catch (error) { console.error("Failed to delete API key", error); res.status(500).json({ error: "Failed to delete API key" }); diff --git a/server/src/middleware/apiMiddleware.ts b/server/src/middleware/apiMiddleware.ts index c227f7d14..1d60ab77e 100644 --- a/server/src/middleware/apiMiddleware.ts +++ b/server/src/middleware/apiMiddleware.ts @@ -1,27 +1,64 @@ import { validateApiKey } from "@/external/unkeyUtils.js"; import { withOrgAuth } from "./authMiddleware.js"; +import { migrateKey, verifyKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; -const API_KEY_LENGTH = 32; export const apiAuthMiddleware = async (req: any, res: any, next: any) => { const authHeader = req.headers["authorization"] || req.headers["Authorization"]; if (!authHeader || !authHeader.startsWith("Bearer ")) { - console.log("Invalid API key / token"); - res.status(401).json({ message: "Invalid API key / token" }); + console.log("No authorization header"); + res + .status(401) + .json({ message: "Unauthorized -- did you forget to add an API key?" }); return; } const apiKey = authHeader.split(" ")[1]; - if (!apiKey.startsWith("am_") || apiKey.length !== API_KEY_LENGTH) { - // console.log("Invalid API Key, verifying clerk token"); + if (!apiKey.startsWith("am_")) { withOrgAuth(req, res, next); return; } + // Try verify via Autumn + try { + const timeStart = Date.now(); + const { valid, data } = await verifyKey({ sb: req.sb, key: apiKey }); + const timeEnd = Date.now(); + console.log(`Time taken to verify key: ${timeEnd - timeStart}ms`); + + if (valid && data) { + console.log( + `Autumn API verification successful for ${data.meta.org_slug} (${data.env})` + ); + req.orgId = data.org_id; + req.env = data.env; + req.minOrg = { + id: data.org_id, + slug: data.meta.org_slug, + }; + + next(); + return; + } else { + console.log(`Autumn API verification failed`); + } + } catch (error) { + console.log("Failed to fetch key from Autumn"); + } + + // Fallback: Verify via Unkey try { const result = await validateApiKey(apiKey); + await migrateKey({ + sb: req.sb, + keyId: result.keyId ?? "", + meta: { org_slug: result.meta?.org_slug }, + apiKey, + }); + + console.log(`Unkey verification successul for ${result.meta?.org_slug}`); req.orgId = result.ownerId; req.env = result.environment; req.minOrg = { @@ -31,39 +68,8 @@ export const apiAuthMiddleware = async (req: any, res: any, next: any) => { next(); } catch (error) { - console.log("Failed to verify API Key"); + console.log("Unkey API verification failed"); withOrgAuth(req, res, next); return; } }; - -export const wsAuthMiddleware = async (req: any) => { - const authHeader = req.headers["authorization"]; - - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return { data: null, error: "Unauthorized" }; - } - - const apiKey = authHeader.split(" ")[1]; - if (!apiKey.startsWith("am_") || apiKey.length !== API_KEY_LENGTH) { - return { data: null, error: "Unauthorized" }; - } - - try { - const result = await validateApiKey(apiKey); - - return { - data: { - env: result.environment, - orgId: result.ownerId, - minOrg: { - id: result.ownerId, - slug: result.meta?.org_slug, - }, - }, - error: null, - }; - } catch (error) { - return { data: null, error: "Unauthorized" }; - } -}; diff --git a/shared/models/devModels/apiKeyModels.ts b/shared/models/devModels/apiKeyModels.ts index e73532393..42889f71a 100644 --- a/shared/models/devModels/apiKeyModels.ts +++ b/shared/models/devModels/apiKeyModels.ts @@ -8,4 +8,6 @@ export type ApiKey = { prefix: string; created_at: number; env: AppEnv; + hashed_key: string; + meta: any; };