</>StackKit
</>StackKit

Developer tutorials & guides

API Authentication: JWT, OAuth 2.0, and API Keys Explained

Understand the three main API authentication methods — API keys, JWT, and OAuth 2.0 — when to use each, and how to implement them securely.

N

Nitheesh DR

Founder & Full-Stack Engineer

9 min read1,001 words
#api#jwt#oauth#authentication#security#rest

Why API Authentication Matters

An unauthenticated API is an open door — anyone can read your data, exhaust your rate limits, or perform actions on behalf of your users. Authentication tells your API who is making a request. Authorization tells it what they're allowed to do.

The three dominant approaches to API authentication are API keys, JSON Web Tokens (JWT), and OAuth 2.0. Each solves a different problem.


API Keys

The simplest method. A long random string that identifies the caller.

Authorization: Bearer sk_live_4bD92jKm9xZqRt7LvPwN3cYa
# or as a query param (less secure):
GET /api/data?api_key=sk_live_4bD92jKm9xZqRt7LvPwN3cYa

How it works:

  1. User generates an API key in your dashboard
  2. Client sends the key with every request
  3. Server looks up the key in the database, finds the associated account, grants access

Implementation (Node.js/Express):

async function apiKeyMiddleware(req, res, next) {
  const key = req.headers.authorization?.replace("Bearer ", "");
  if (!key) return res.status(401).json({ error: "Missing API key" });

  const account = await db.apiKeys.findOne({ key, active: true });
  if (!account) return res.status(401).json({ error: "Invalid API key" });

  req.account = account;
  next();
}

Generating a secure key:

import crypto from "crypto";
const apiKey = "sk_live_" + crypto.randomBytes(32).toString("hex");

Pros: Simple to implement and use, stateless lookup, easy to revoke Cons: If leaked, full access until manually revoked. No expiry. Not suitable for user-facing apps.

Best for: Server-to-server integrations, developer tools, internal services, third-party API access (Stripe, Twilio, Unsplash all use this).


JWT (JSON Web Tokens)

A JWT is a self-contained token that carries claims about the user. The server doesn't need a database lookup to verify it — it just checks the signature.

JWT structure (three base64url-encoded parts separated by dots):

header.payload.signature

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMiLCJleHAiOjE3MTk4MDAwMDB9.abc123

Header: Algorithm used (HS256, RS256) Payload: Claims — user data, expiry, roles Signature: HMAC of header + payload, signed with your secret key

Generating a JWT (Node.js with jsonwebtoken):

import jwt from "jsonwebtoken";

const token = jwt.sign(
  {
    userId: user.id,
    email: user.email,
    role: "admin",
  },
  process.env.JWT_SECRET,
  { expiresIn: "15m" } // short-lived
);

Verifying a JWT:

function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (!token) return res.status(401).json({ error: "Missing token" });

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.user = payload;
    next();
  } catch {
    return res.status(401).json({ error: "Invalid or expired token" });
  }
}

Access + Refresh Token Pattern: Short-lived access tokens (15 min) + long-lived refresh tokens (7 days) stored in httpOnly cookies:

// Access token — short lived, sent in Authorization header
const accessToken = jwt.sign({ userId }, JWT_SECRET, { expiresIn: "15m" });

// Refresh token — long lived, stored in httpOnly cookie
const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: "7d" });
res.cookie("refreshToken", refreshToken, {
  httpOnly: true,  // not accessible by JS — prevents XSS theft
  secure: true,    // HTTPS only
  sameSite: "strict",
});

Pros: No database lookup per request (stateless), carries user data in the token, works across microservices Cons: Can't be revoked before expiry (without a blocklist), payload is base64-encoded (not encrypted — don't put secrets in it)

Best for: User authentication in SPAs and mobile apps, microservice-to-microservice auth, stateless sessions.


OAuth 2.0

OAuth 2.0 is an authorization framework that lets your app access resources on behalf of a user at a third-party service — without ever seeing their password.

When you click "Sign in with Google," that's OAuth 2.0.

The Authorization Code Flow (most secure):

  1. User clicks "Login with GitHub"
  2. You redirect to GitHub's authorization page
  3. User approves access
  4. GitHub redirects back to your app with an authorization code
  5. Your server exchanges the code for an access token (server-to-server, code never exposed in browser)
  6. Use the access token to call GitHub's API
// Step 2: Redirect to GitHub
const githubAuthUrl = new URL("https://github.com/login/oauth/authorize");
githubAuthUrl.searchParams.set("client_id", process.env.GITHUB_CLIENT_ID);
githubAuthUrl.searchParams.set("redirect_uri", "https://yourapp.com/auth/callback");
githubAuthUrl.searchParams.set("scope", "user:email");
githubAuthUrl.searchParams.set("state", generateRandomState()); // CSRF protection
res.redirect(githubAuthUrl.toString());

// Step 5: Exchange code for token
app.get("/auth/callback", async (req, res) => {
  const { code, state } = req.query;
  // verify state matches what you sent (CSRF check)

  const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
    method: "POST",
    headers: { "Content-Type": "application/json", "Accept": "application/json" },
    body: JSON.stringify({
      client_id: process.env.GITHUB_CLIENT_ID,
      client_secret: process.env.GITHUB_CLIENT_SECRET,
      code,
    }),
  });
  const { access_token } = await tokenResponse.json();

  // Step 6: Use token to get user info
  const userResponse = await fetch("https://api.github.com/user", {
    headers: { Authorization: `Bearer ${access_token}` },
  });
  const githubUser = await userResponse.json();
  // Create or log in your user based on githubUser.id
});

Pros: Users never share passwords with your app, delegates trust to a provider, industry standard Cons: Complex flow, dependent on third-party availability

Best for: "Login with Google/GitHub/Facebook," accessing third-party APIs on behalf of users.


Comparison Table

API Keys JWT OAuth 2.0
Complexity Low Medium High
Revocable Yes (manual) Only with blocklist Yes
Stateless No (DB lookup) Yes No (token exchange)
User identity No Yes Yes
Third-party access No No Yes
Best for M2M, developer APIs User auth, SPAs Login with X

Security Rules for All Methods

  1. Always use HTTPS — tokens in plaintext are stolen instantly on plain HTTP
  2. Never log tokens or API keys — keep them out of your logs
  3. Use short expiry times — 15 minutes for access tokens
  4. Store tokens in httpOnly cookies on the web (not localStorage — XSS can steal it)
  5. Rotate secrets if they're exposed
  6. Validate and sanitize everything in the payload

Conclusion

Use API keys for server-to-server and developer integrations. Use JWT for user authentication in your own app. Use OAuth 2.0 when you need to log users in with a third-party provider or access their data on external services. Each method has its place — the worst choice is mixing them without understanding the security tradeoffs.

Tagged

#api#jwt#oauth#authentication#security#rest
N

Written by

Nitheesh DR

Founder & Full-Stack Engineer

Nitheesh is a full-stack software engineer based in Tamil Nadu, India, with hands-on experience building production SaaS applications using Next.js, TypeScript, React, Node.js, and cloud infrastructure. He founded StackKit to share the practical knowledge he uses every day — not just theory, but the real-world techniques that help developers ship better software faster.