DTTooleras

API Authentication Methods Compared: API Keys, OAuth 2.0, JWT, and More

A practical comparison of API authentication methods — API keys, Basic Auth, Bearer tokens, OAuth 2.0, JWT, and API signatures. When to use each, security trade-offs, and implementation examples.

DevToolsHub Team22 min read1,283 words

Why API Authentication Matters

Every API needs to answer two questions:

  1. Authentication — Who are you?
  2. Authorization — What are you allowed to do?

Choosing the right authentication method depends on your use case, security requirements, and who your API consumers are. This guide compares the most common approaches.

1. API Keys

The simplest form of API authentication. A unique string is assigned to each client and sent with every request.

How It Works

GET /api/data HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_abc123def456

Or as a query parameter (less secure):

GET /api/data?api_key=sk_live_abc123def456

Implementation

// Server-side validation (Express)
function apiKeyAuth(req, res, next) {
  const apiKey = req.headers["x-api-key"];

  if (!apiKey) {
    return res.status(401).json({ error: "API key required" });
  }

  // Look up the key in your database
  const client = await db.clients.findOne({ apiKey });

  if (!client) {
    return res.status(403).json({ error: "Invalid API key" });
  }

  req.client = client;
  next();
}

app.use("/api", apiKeyAuth);

Pros and Cons

Pros:

  • Simple to implement and use
  • Easy to revoke (delete the key)
  • Good for server-to-server communication
  • Easy to rate limit per key

Cons:

  • No built-in expiration
  • If leaked, full access until revoked
  • No user context (identifies the app, not the user)
  • Must be kept secret (never in client-side code)

Best for: Server-to-server APIs, third-party integrations, public APIs with rate limiting.

2. HTTP Basic Authentication

Sends username and password encoded in Base64 with every request.

How It Works

GET /api/data HTTP/1.1
Authorization: Basic YWxpY2U6cGFzc3dvcmQxMjM=

The value after "Basic" is base64("alice:password123").

Implementation

function basicAuth(req, res, next) {
  const auth = req.headers.authorization;

  if (!auth || !auth.startsWith("Basic ")) {
    res.setHeader("WWW-Authenticate", "Basic realm=\"API\"");
    return res.status(401).json({ error: "Authentication required" });
  }

  const credentials = Buffer.from(auth.split(" ")[1], "base64").toString();
  const [username, password] = credentials.split(":");

  const user = await verifyCredentials(username, password);
  if (!user) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  req.user = user;
  next();
}

Pros and Cons

Pros:

  • Very simple to implement
  • Supported by every HTTP client
  • Built into browsers (login popup)

Cons:

  • Credentials sent with every request (even if Base64-encoded, NOT encrypted)
  • Must use HTTPS — Base64 is trivially decoded
  • No logout mechanism (browser caches credentials)
  • Password sent repeatedly increases exposure risk

Best for: Internal tools, simple scripts, when combined with HTTPS. Generally avoid for public APIs.

3. Bearer Tokens (JWT)

A token-based approach where the client receives a token after authentication and sends it with subsequent requests.

How It Works

POST /auth/login
{"email": "alice@example.com", "password": "secret"}

→ {"access_token": "eyJhbGciOiJIUzI1NiIs...", "expires_in": 900}

GET /api/data
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Implementation

// Login endpoint
app.post("/auth/login", async (req, res) => {
  const { email, password } = req.body;
  const user = await verifyCredentials(email, password);

  if (!user) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const token = jwt.sign(
    { sub: user.id, email: user.email, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: "15m" }
  );

  res.json({ access_token: token, token_type: "Bearer", expires_in: 900 });
});

// Protected route middleware
function bearerAuth(req, res, next) {
  const auth = req.headers.authorization;

  if (!auth || !auth.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Bearer token required" });
  }

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

Pros and Cons

Pros:

  • Stateless — no server-side session storage needed
  • Contains user info (no database lookup per request)
  • Built-in expiration
  • Works across domains and services

Cons:

  • Can't be revoked before expiration (without a blacklist)
  • Token size can be large (sent with every request)
  • Must handle token refresh logic
  • Payload is readable (not encrypted)

Best for: SPAs, mobile apps, microservices, any API that needs user context.

4. OAuth 2.0

A delegation framework that lets users grant third-party apps limited access to their resources without sharing credentials.

The Authorization Code Flow

This is the most common OAuth flow for web applications:

1. User clicks "Login with Google" on your app
2. Your app redirects to Google's authorization server
3. User logs in to Google and grants permission
4. Google redirects back to your app with an authorization code
5. Your server exchanges the code for an access token
6. Your server uses the access token to call Google's API
User → Your App → Google Auth → User Login → Google Auth → Your App → Google API
         |                                                      |
    redirect to                                          exchange code
    Google login                                         for token

Implementation (Simplified)

// Step 1: Redirect to provider
app.get("/auth/google", (req, res) => {
  const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
  url.searchParams.set("client_id", process.env.GOOGLE_CLIENT_ID);
  url.searchParams.set("redirect_uri", "https://myapp.com/auth/callback");
  url.searchParams.set("response_type", "code");
  url.searchParams.set("scope", "openid email profile");
  url.searchParams.set("state", generateRandomState());
  res.redirect(url.toString());
});

// Step 2: Handle callback
app.get("/auth/callback", async (req, res) => {
  const { code, state } = req.query;

  // Verify state to prevent CSRF
  if (!verifyState(state)) {
    return res.status(400).json({ error: "Invalid state" });
  }

  // Exchange code for token
  const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      code,
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      redirect_uri: "https://myapp.com/auth/callback",
      grant_type: "authorization_code",
    }),
  });

  const { access_token } = await tokenResponse.json();

  // Use token to get user info
  const userInfo = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
    headers: { Authorization: `Bearer ${access_token}` },
  }).then(r => r.json());

  // Create session for user
  // ...
});

OAuth 2.0 Grant Types

Grant TypeUse Case
Authorization CodeWeb apps with a backend
Authorization Code + PKCESPAs, mobile apps
Client CredentialsServer-to-server (no user)
Device CodeSmart TVs, CLI tools

Best for: "Login with Google/GitHub/etc.", third-party API access, when users need to grant limited permissions.

5. API Signatures (HMAC)

Requests are signed with a secret key. The server verifies the signature to authenticate the request.

How It Works

// Client signs the request
const timestamp = Date.now().toString();
const payload = `${method}\n${path}\n${timestamp}\n${body}`;
const signature = crypto
  .createHmac("sha256", apiSecret)
  .update(payload)
  .digest("hex");

fetch("https://api.example.com/data", {
  method: "POST",
  headers: {
    "X-API-Key": apiKey,
    "X-Timestamp": timestamp,
    "X-Signature": signature,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
});

Pros and Cons

Pros:

  • Secret never sent over the wire
  • Prevents request tampering
  • Replay protection (with timestamp)
  • Very secure

Cons:

  • Complex to implement
  • Clock synchronization required
  • Harder to debug
  • Not suitable for browser-based clients

Best for: Payment APIs (Stripe webhooks), high-security APIs, webhook verification.

Comparison Table

MethodComplexitySecurityUser ContextStatelessBest For
API KeyLowMediumNoYesServer-to-server
Basic AuthLowLowYesYesInternal tools
Bearer/JWTMediumHighYesYesSPAs, mobile apps
OAuth 2.0HighHighYesDependsThird-party login
HMAC SignatureHighVery HighNoYesPayment/webhook APIs

Security Best Practices

  1. Always use HTTPS — Every method above is insecure over plain HTTP
  2. Rotate credentials regularly — API keys, secrets, and tokens should have expiration
  3. Use the principle of least privilege — Grant only the permissions needed
  4. Rate limit all endpoints — Prevent brute-force attacks
  5. Log authentication events — Track failed attempts for security monitoring
  6. Never store secrets in client-side code — Use environment variables and server-side proxies
  7. Validate tokens server-side — Don't trust client-provided claims without verification

Decode and inspect your JWT tokens with our JWT Decoder tool, and generate HMAC signatures with our HMAC Generator.

api authenticationoauthjwtapi keybearer tokenapi securityauthenticationauthorization

Related articles

All articles

Practice with free tools

200+ free developer tools that run in your browser.

Browse all tools →