DEV Community

Young Gao
Young Gao

Posted on

API Authentication Done Right: JWTs, API Keys, and OAuth2 in Production (2026 Guide)

Every backend faces the same question: how do we authenticate requests?


Auth Model Comparison

Feature API Keys JWTs OAuth2
Best for Server-to-server User sessions, SPAs Delegated access
Stateless? No Yes Depends
Revocable? Instantly Not until expiry Yes
Complexity Low Medium High

1. API Key Authentication

Two rules: never store keys in plaintext, always use a prefix.

function generateApiKey() {
  const prefix = "pk_live";
  const secret = crypto.randomBytes(32).toString("base64url");
  const fullKey = `${prefix}_${secret}`;
  const hash = crypto.createHash("sha256").update(fullKey).digest("hex");
  return { key: fullKey, record: { id: crypto.randomUUID(), prefix, hash, createdAt: new Date(), lastUsedAt: null } };
}
Enter fullscreen mode Exit fullscreen mode
async function apiKeyAuth(req: Request, res: Response, next: NextFunction) {
  const header = req.headers["x-api-key"];
  if (!header) return res.status(401).json({ error: "Missing API key" });
  const hash = crypto.createHash("sha256").update(header).digest("hex");
  const record = await db.apiKeys.findOne({ hash });
  if (!record) return res.status(401).json({ error: "Invalid API key" });
  req.apiClient = record;
  next();
}
Enter fullscreen mode Exit fullscreen mode

2. JWT Authentication

Token Generation

function generateTokenPair(user: { sub: string; email: string; roles: string[] }) {
  const accessToken = jwt.sign(
    { sub: user.sub, email: user.email, roles: user.roles },
    ACCESS_SECRET,
    { expiresIn: "15m", issuer: "api.yourapp.com", audience: "yourapp.com" }
  );
  const refreshToken = jwt.sign(
    { sub: user.sub, type: "refresh" },
    REFRESH_SECRET,
    { expiresIn: "7d", issuer: "api.yourapp.com", jwtid: crypto.randomUUID() }
  );
  return { accessToken, refreshToken };
}
Enter fullscreen mode Exit fullscreen mode

Verification Middleware

function jwtAuth(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith("Bearer ")) return res.status(401).json({ error: "Missing token" });
  try {
    req.user = jwt.verify(authHeader.slice(7), ACCESS_SECRET, {
      issuer: "api.yourapp.com", audience: "yourapp.com", algorithms: ["HS256"]
    });
    next();
  } catch (err) {
    return res.status(401).json({ error: "Invalid token" });
  }
}
Enter fullscreen mode Exit fullscreen mode

Role-Based Access

function requireRoles(...roles: string[]) {
  return (req, res, next) => {
    if (!roles.some(r => req.user?.roles?.includes(r))) {
      return res.status(403).json({ error: "Forbidden" });
    }
    next();
  };
}
app.delete("/api/users/:id", jwtAuth, requireRoles("admin"), deleteUser);
Enter fullscreen mode Exit fullscreen mode

3. OAuth2 Authorization Code Flow

Token Exchange

async function exchangeCode(code: string, config: OAuth2Config, codeVerifier?: string) {
  const params = new URLSearchParams({
    grant_type: "authorization_code", code,
    redirect_uri: config.redirectUri, client_id: config.clientId,
  });
  if (config.clientSecret) params.set("client_secret", config.clientSecret);
  if (codeVerifier) params.set("code_verifier", codeVerifier);
  const res = await fetch(config.tokenUrl, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: params.toString(),
  });
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

PKCE

function generatePKCE() {
  const codeVerifier = crypto.randomBytes(32).toString("base64url");
  const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
  return { codeVerifier, codeChallenge, method: "S256" };
}
Enter fullscreen mode Exit fullscreen mode

4. Layered Auth Middleware

function authenticate(...strategies: AuthStrategy[]) {
  const handlers = { apiKey: apiKeyAuth, jwt: jwtAuth, oauth2: oauth2Auth };
  return async (req, res, next) => {
    for (const s of strategies) {
      try {
        await new Promise((resolve, reject) => {
          handlers[s](req, res, err => err ? reject(err) : resolve());
        });
        return next();
      } catch {}
    }
    res.status(401).json({ error: "Auth required" });
  };
}

app.get("/api/v1/data", authenticate("apiKey", "oauth2"), dataHandler);
app.get("/api/admin", authenticate("jwt"), requireRoles("admin"), adminHandler);
Enter fullscreen mode Exit fullscreen mode

Security Checklist

  • [ ] Rate-limit auth endpoints
  • [ ] Hash API keys with SHA-256
  • [ ] Rotate secrets on schedule
  • [ ] HTTPS only
  • [ ] Validate all JWT claims (iss, aud, exp, alg)
  • [ ] Use PKCE everywhere
  • [ ] Short access token TTLs (15min max)
  • [ ] Secure cookies: HttpOnly, Secure, SameSite=Strict
  • [ ] Lock down CORS
  • [ ] Log auth events

API keys for machines. JWTs for users. OAuth2 for delegation. Layer them together.

Production Backend Patterns series


If this was useful, consider:


You Might Also Like

Follow me for more production-ready backend content!


If this helped you, buy me a coffee on Ko-fi!

Top comments (0)