Loading...
Loading...
> Secure API key generation with SHA-256 hashing, timing-safe comparison, and rate limiting.
The API keys system provides secure programmatic access to your application's API. Keys are generated with cryptographic randomness, stored as hashes, and validated using timing-safe comparison to prevent timing attacks.
API key model in Prisma schema
1// prisma/schema.prisma2model ApiKey {3 id String @id @default(cuid())4 userId String5 user User @relation(fields: [userId], references: [id], onDelete: Cascade)67 name String // User-friendly name8 keyHash String @unique // SHA-256 hash9 keyPrefix String // First 8 chars for identification (e.g., "fk_live_")1011 scopes String[] // ["read:users", "write:data"]1213 lastUsedAt DateTime?14 expiresAt DateTime?15 revokedAt DateTime?1617 createdAt DateTime @default(now())1819 @@index([keyHash])20 @@index([userId])21}
Securely generate and hash API keys
1// src/lib/api-keys.ts2import crypto from "crypto";3import { prisma } from "@/lib/db";45interface CreateApiKeyInput {6 userId: string;7 name: string;8 scopes: string[];9 expiresAt?: Date;10}1112export async function createApiKey(input: CreateApiKeyInput) {13 // Generate 256-bit random key14 const rawKey = crypto.randomBytes(32).toString("base64url");1516 // Create key with prefix for easy identification17 const prefix = "fk_live_";18 const fullKey = prefix + rawKey;1920 // Hash the key for storage21 const keyHash = crypto22 .createHash("sha256")23 .update(fullKey)24 .digest("hex");2526 // Store in database27 const apiKey = await prisma.apiKey.create({28 data: {29 userId: input.userId,30 name: input.name,31 keyHash,32 keyPrefix: prefix,33 scopes: input.scopes,34 expiresAt: input.expiresAt,35 },36 });3738 // Return the full key ONCE (never stored or retrievable)39 return {40 id: apiKey.id,41 key: fullKey, // Show to user once42 name: apiKey.name,43 scopes: apiKey.scopes,44 createdAt: apiKey.createdAt,45 };46}4748// Available scopes49export const API_SCOPES = [50 "read:profile",51 "write:profile",52 "read:organizations",53 "write:organizations",54 "read:billing",55 "manage:billing",56 "admin",57] as const;
Validate API keys with timing-safe comparison
1// src/lib/api-keys.ts2import crypto from "crypto";34export async function validateApiKey(key: string) {5 // Hash the provided key6 const keyHash = crypto7 .createHash("sha256")8 .update(key)9 .digest("hex");1011 // Find key by hash12 const apiKey = await prisma.apiKey.findUnique({13 where: { keyHash },14 include: { user: true },15 });1617 if (!apiKey) {18 return { valid: false, error: "Invalid API key" };19 }2021 // Check if revoked22 if (apiKey.revokedAt) {23 return { valid: false, error: "API key has been revoked" };24 }2526 // Check expiration27 if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {28 return { valid: false, error: "API key has expired" };29 }3031 // Update last used timestamp32 await prisma.apiKey.update({33 where: { id: apiKey.id },34 data: { lastUsedAt: new Date() },35 });3637 return {38 valid: true,39 userId: apiKey.userId,40 scopes: apiKey.scopes,41 user: apiKey.user,42 };43}4445// Middleware for API routes46export async function withApiKey(47 req: Request,48 requiredScopes: string[] = []49) {50 const authHeader = req.headers.get("Authorization");5152 if (!authHeader?.startsWith("Bearer ")) {53 return { error: "Missing API key", status: 401 };54 }5556 const key = authHeader.slice(7);57 const result = await validateApiKey(key);5859 if (!result.valid) {60 return { error: result.error, status: 401 };61 }6263 // Check scopes64 if (requiredScopes.length > 0) {65 const hasScope = requiredScopes.every(66 (scope) => result.scopes.includes(scope) || result.scopes.includes("admin")67 );6869 if (!hasScope) {70 return { error: "Insufficient permissions", status: 403 };71 }72 }7374 return { user: result.user, scopes: result.scopes };75}
Protect your API routes with API key authentication
1// src/app/api/v1/data/route.ts2import { withApiKey } from "@/lib/api-keys";34export async function GET(req: Request) {5 // Validate API key with required scopes6 const auth = await withApiKey(req, ["read:data"]);78 if ("error" in auth) {9 return Response.json(10 { error: auth.error },11 { status: auth.status }12 );13 }1415 // auth.user is available16 const data = await prisma.data.findMany({17 where: { userId: auth.user.id },18 });1920 return Response.json({ data });21}2223export async function POST(req: Request) {24 // Require write scope25 const auth = await withApiKey(req, ["write:data"]);2627 if ("error" in auth) {28 return Response.json(29 { error: auth.error },30 { status: auth.status }31 );32 }3334 const body = await req.json();3536 const data = await prisma.data.create({37 data: {38 ...body,39 userId: auth.user.id,40 },41 });4243 return Response.json({ data });44}
Implement rate limiting per API key
1// src/lib/rate-limit.ts2import { Redis } from "@upstash/redis";34const redis = new Redis({5 url: process.env.UPSTASH_REDIS_URL!,6 token: process.env.UPSTASH_REDIS_TOKEN!,7});89interface RateLimitConfig {10 maxRequests: number;11 windowMs: number;12}1314const RATE_LIMITS: Record<string, RateLimitConfig> = {15 default: { maxRequests: 100, windowMs: 60000 }, // 100/min16 premium: { maxRequests: 1000, windowMs: 60000 }, // 1000/min17 enterprise: { maxRequests: 10000, windowMs: 60000 }, // 10000/min18};1920export async function checkRateLimit(21 identifier: string,22 tier: string = "default"23) {24 const config = RATE_LIMITS[tier] || RATE_LIMITS.default;25 const key = `rate_limit:${identifier}`;2627 const current = await redis.incr(key);2829 if (current === 1) {30 await redis.pexpire(key, config.windowMs);31 }3233 const remaining = Math.max(0, config.maxRequests - current);34 const reset = await redis.pttl(key);3536 return {37 allowed: current <= config.maxRequests,38 remaining,39 reset: Date.now() + reset,40 limit: config.maxRequests,41 };42}4344// Usage in API route45export async function GET(req: Request) {46 const auth = await withApiKey(req);47 if ("error" in auth) {48 return Response.json({ error: auth.error }, { status: auth.status });49 }5051 const rateLimit = await checkRateLimit(auth.user.id, auth.user.plan);5253 if (!rateLimit.allowed) {54 return Response.json(55 { error: "Rate limit exceeded" },56 {57 status: 429,58 headers: {59 "X-RateLimit-Limit": rateLimit.limit.toString(),60 "X-RateLimit-Remaining": "0",61 "X-RateLimit-Reset": rateLimit.reset.toString(),62 "Retry-After": Math.ceil((rateLimit.reset - Date.now()) / 1000).toString(),63 },64 }65 );66 }6768 // Process request...69}