Skip to content

API Keys — Complete Implementation Guide for Service Auth

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about API Keys. We cover key concepts, practical examples, and best practices to help you master this topic.

API keys are static tokens used to authenticate service-to-service communication and developer API access, providing a simple but effective authentication mechanism where a shared secret identifies the calling application.

What You'll Learn

By the end of this lesson, you will implement secure API key generation, validation, hashing for storage, rotation policies, and build a complete API key management system with tiers and permissions.

Why It Matters

API keys are the most common authentication method for public developer APIs. Services like Stripe, Twilio, and GitHub use API keys to authenticate millions of API calls daily. Doda Browser's public API uses API keys to authenticate third-party developers building on the platform.

Real-World Use

A developer signs up for a weather API service. The dashboard generates two API keys: a live key for production and a test key for development. The developer includes the key in the X-API-Key header. The API validates the key, checks the rate limit tier, and processes the request.

API Key Security Flow

flowchart LR
    A[Developer Portal] -->|Generate| B[API Key: sk_live_abc123]
    B --> C[Hash & Store in DB]
    B --> D[Show to Developer Once]
    D --> E[Developer uses in API calls]
    E --> F[Server hashes incoming key]
    F --> G[Compare with stored hashes]
    G -->|Match| H[Authenticate Request]
    G -->|No Match| I[Reject 401]

Secure API Key Generation

import secrets
import hashlib
import hmac
from datetime import datetime, timedelta

class APIKeyManager:
    def __init__(self):
        self.stored_keys = {}

    def generate_key(self, client_name, tier="standard"):
        prefix = "sk_live" if tier != "test" else "sk_test"
        random_part = secrets.token_urlsafe(32)
        api_key = f"{prefix}_{random_part}"

        key_hash = hashlib.sha256(api_key.encode()).hexdigest()
        key_id = secrets.token_hex(8)

        self.stored_keys[key_hash] = {
            "key_id": key_id,
            "client_name": client_name,
            "tier": tier,
            "created_at": datetime.utcnow(),
            "expires_at": datetime.utcnow() + timedelta(days=365),
            "is_active": True,
        }

        print(f"[APIKey] Key generated for {client_name}")
        print(f"[APIKey] Key ID: {key_id}")
        print(f"[APIKey] IMPORTANT: Store this key securely - it won't be shown again:")
        print(f"  {api_key[:12]}...{api_key[-4:]}")
        return {"api_key": api_key, "key_id": key_id, "tier": tier}

    def validate_key(self, api_key):
        key_hash = hashlib.sha256(api_key.encode()).hexdigest()
        key_data = self.stored_keys.get(key_hash)

        if not key_data:
            print(f"[APIKey] Invalid key rejected")
            return None

        if not key_data["is_active"]:
            print(f"[APIKey] Deactivated key rejected")
            return None

        if datetime.utcnow() > key_data["expires_at"]:
            print(f"[APIKey] Expired key rejected")
            return None

        print(f"[APIKey] Key validated: {key_data['client_name']} ({key_data['tier']})")
        return key_data

    def revoke_key(self, key_id):
        for key_hash, data in self.stored_keys.items():
            if data["key_id"] == key_id:
                data["is_active"] = False
                print(f"[APIKey] Key {key_id} revoked")
                return True
        return False

    def rotate_key(self, old_api_key, client_name):
        old_data = self.validate_key(old_api_key)
        if not old_data:
            return None
        self.revoke_key(old_data["key_id"])
        return self.generate_key(client_name, old_data["tier"])

manager = APIKeyManager()
result = manager.generate_key("Acme Corp", "premium")
print(f"\nKey preview: {result['api_key'][:12]}...")
validation = manager.validate_key(result["api_key"])
print(f"Validated: {validation['client_name'] if validation else 'FAILED'}")

Expected output:

[APIKey] Key generated for Acme Corp
[APIKey] Key ID: a1b2c3d4
[APIKey] IMPORTANT: Store this key securely...
  sk_live_a1b2...c3d4
[APIKey] Key validated: Acme Corp (premium)
Validated: Acme Corp

API Key Validation in Express

const crypto = require("crypto");

class APIKeyAuthenticator {
  constructor() {
    this.keys = new Map();
  }

  addKey(apiKey, client) {
    const hash = crypto.createHash("sha256").update(apiKey).digest("hex");
    this.keys.set(hash, {
      clientId: client.id,
      clientName: client.name,
      tier: client.tier || "standard",
      rateLimit: client.rateLimit || 100,
    });
  }

  authenticate(req, res, next) {
    const apiKey = req.headers["x-api-key"] || req.query.api_key;
    if (!apiKey) {
      return res.status(401).json({ error: "API key required" });
    }

    const hash = crypto.createHash("sha256").update(apiKey).digest("hex");
    const keyData = this.keys.get(hash);

    if (!keyData) {
      console.log(`[Auth] Invalid API key from ${req.ip}`);
      return res.status(403).json({ error: "Invalid API key" });
    }

    req.client = keyData;
    console.log(`[Auth] Authenticated: ${keyData.clientName} (${keyData.tier})`);
    next();
  }
}

const auth = new APIKeyAuthenticator();
auth.addKey("sk_live_test_key_123", {
  id: "client_1",
  name: "Acme Corp",
  tier: "premium",
  rateLimit: 10000,
});

module.exports = auth;

Common Mistakes

  1. Storing API keys in plain text in the database instead of hashed, exposing all keys on data breach.
  2. Generating predictable keys (sequential, timestamp-based) that can be guessed.
  3. Embedding API keys in client-side code (mobile apps, SPAs) where they can be extracted.
  4. Not supporting key rotation forces developers to keep the same key indefinitely.
  5. Using API keys without Rate Limiting allows a compromised key to abuse the API.
  6. Logging full API keys in request logs exposes them in the logging pipeline.

Practice Questions

  1. Why should API keys be hashed before storing in the database?

If the database is breached, plain-text API keys are immediately compromised. Hashing (SHA-256) means the attacker only has hashes, which cannot be reversed to valid keys.

  1. What is the difference between a key prefix and a key ID?

The prefix identifies the key type (sk_live, sk_test). The key ID is a public identifier used to look up which key made a request (shown in logs, not secret). The full key is the secret.

  1. How do you support multiple API keys per client?

Allow each client to generate up to N keys (typically 5-10). Each key has its own rate limit, permissions, and can be individually revoked. This enables key rotation without downtime.

  1. Challenge: Build a complete API key management system with generation, hashing storage, validation, tiers (free/pro/enterprise) with different rate limits, rotation endpoint, and automated expiry warnings.

FAQ

Should API keys have expiration?

Yes. Set keys to expire after a reasonable period (6-12 months). Send expiration warnings 30 days before expiry. Allow automatic renewal for trusted clients with unchanged permissions.

How do I handle API key leakage?

Implement immediate key revocation through the developer dashboard. Monitor for unusual usage patterns (geographic anomalies, sudden traffic spikes). Alert developers on suspected compromise.

Can I use API keys without HTTPS?

No. API keys are bearer tokens — anyone who possesses the key can use it. Without HTTPS, the key is exposed to network interception. Always require HTTPS for API key authentication.

What rate limit should I set per tier?

Free: 10-100 req/min. Standard: 1000 req/min. Premium: 10000 req/min. Enterprise: Custom. Return rate limit headers so developers can monitor their usage.

Mini Project: Developer API Key Dashboard

Build a CLI dashboard that manages API keys for multiple clients, showing active keys, usage statistics, expiry dates, and allowing key generation and revocation.

import json
import secrets
import hashlib
from datetime import datetime, timedelta

class APIDashboard:
    def __init__(self, storage_path="api_keys.json"):
        self.path = storage_path
        self.load()

    def load(self):
        try:
            with open(self.path) as f:
                self.data = json.load(f)
        except FileNotFoundError:
            self.data = {"clients": {}, "keys": {}}

    def save(self):
        with open(self.path, "w") as f:
            json.dump(self.data, f, indent=2, default=str)

    def list_keys(self):
        print(f"{'Client':<20} {'Key ID':<12} {'Tier':<10} {'Status':<10} {'Expires':<15}")
        print("=" * 70)
        for hash_val, key_data in self.data["keys"].items():
            print(f"{key_data['client']:<20} {key_data['key_id']:<12} {key_data['tier']:<10} {'Active' if key_data['active'] else 'Revoked':<10} {key_data['expires']:<15}")

    def generate(self, client, tier="standard"):
        key = f"sk_live_{secrets.token_urlsafe(32)}"
        key_hash = hashlib.sha256(key.encode()).hexdigest()
        key_id = secrets.token_hex(6)
        self.data["keys"][key_hash] = {
            "key_id": key_id, "client": client, "tier": tier,
            "active": True, "created": str(datetime.utcnow()),
            "expires": str(datetime.utcnow() + timedelta(days=365)),
        }
        self.save()
        print(f"Generated key: {key[:12]}...{key[-4:]} (ID: {key_id})")
        print("Store this key securely - it will not be shown again")

    def revoke(self, key_id):
        for k, v in self.data["keys"].items():
            if v["key_id"] == key_id:
                v["active"] = False
                self.save()
                print(f"Key {key_id} revoked")
                return
        print("Key not found")

What's Next

Learn about Basic authentication for legacy compatibility, then explore token refresh patterns for managing long-lived sessions.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro