Skip to content

API Key Authentication at the Gateway — Key Management and Security

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you'll learn about API Key Gateway. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

API keys are the most common authentication method for machine-to-machine communication, and the gateway is the ideal place to validate them before requests reach your services.

What You'll Learn

By the end of this lesson, you will implement API key generation with secure hashing, key rotation and revocation, per-key Rate Limiting, and key validation at the gateway level.

Why It Matters

API keys are simpler than OAuth2 for internal services but require proper lifecycle management to prevent unauthorized access when keys are compromised.

Real-World Use

Durga Antivirus Pro issues API keys to partner services for threat intelligence queries, with gateway-enforced rate limits of 1000 requests per hour per key and automatic rotation every 90 days.

API Key Flow

flowchart LR
    Client-->Request[Request + API Key]
    Request-->Gateway
    Gateway-->Hash[Hash Key]
    Hash-->DB[(Key Store)]
    DB-->Validate{Validate}
    Validate-->|Valid|Rate[Rate Limit]
    Validate-->|Invalid|403[403 Forbidden]
    Rate-->|Under Limit|Backend
    Rate-->|Over Limit|429[429 Too Many]

Secure Key Generation

Generate API keys that are cryptographically random and include a prefix for identification.

import secrets
import hashlib
from typing import Tuple, Dict, Optional
from datetime import datetime, timedelta

class APIKeyGenerator:
    PREFIX = "dag"

    @staticmethod
    def generate_key(client_name: str = "",
                     entropy_bytes: int = 32
                     ) -> Tuple[str, str]:
        raw = f"{APIKeyGenerator.PREFIX}_{secrets.token_hex(entropy_bytes)}"
        prefix = hashlib.sha256(raw.encode()).hexdigest()[:8]
        return prefix, raw

    @staticmethod
    def hash_key(raw_key: str) -> str:
        return hashlib.sha256(raw_key.encode()).hexdigest()

    @staticmethod
    def validate_format(raw_key: str) -> bool:
        return raw_key.startswith(f"{APIKeyGenerator.PREFIX}_")

key_id, raw_key = APIKeyGenerator.generate_key("scan-service")
print(f"Key ID: {key_id}")
print(f"Raw key (show once): {raw_key}")
print(f"Hashed: {APIKeyGenerator.hash_key(raw_key)[:16]}...")
print(f"Format valid: {APIKeyGenerator.validate_format(raw_key)}")

API Key Store and Validation

Store hashed keys in a database and validate them on every request.

import hashlib
import time
from typing import Dict, Optional, Tuple
from collections import defaultdict

class APIKeyStore:
    def __init__(self):
        self.keys: Dict[str, Dict] = {}
        self.key_lookup: Dict[str, str] = {}

    def store_key(self, client_id: str,
                  key_id: str, raw_key: str,
                  ttl_days: int = 90,
                  rate_limit: int = 1000) -> Dict:
        hashed = hashlib.sha256(raw_key.encode()).hexdigest()
        key_data = {
            "client_id": client_id,
            "key_id": key_id,
            "hashed": hashed,
            "created": time.time(),
            "expires": time.time() + ttl_days * 86400,
            "active": True,
            "rate_limit": rate_limit,
            "usage_count": 0,
        }
        self.keys[key_id] = key_data
        self.key_lookup[hashed] = key_id
        return {
            "key_id": key_id,
            "raw_key": raw_key,
            "message": "Store this key securely. It will not be shown again."
        }

    def validate_key(self, raw_key: str
                     ) -> Tuple[bool, Optional[str]]:
        if not raw_key.startswith("dag_"):
            return False, "Invalid key format"
        hashed = hashlib.sha256(raw_key.encode()).hexdigest()
        key_id = self.key_lookup.get(hashed)
        if not key_id:
            return False, "Unknown key"
        key_data = self.keys.get(key_id)
        if not key_data:
            return False, "Key not found"
        if not key_data["active"]:
            return False, "Key revoked"
        if time.time() > key_data["expires"]:
            return False, "Key expired"
        key_data["usage_count"] += 1
        return True, key_id

    def revoke_key(self, key_id: str) -> bool:
        if key_id in self.keys:
            self.keys[key_id]["active"] = False
            return True
        return False

    def rotate_key(self, key_id: str,
                   ttl_days: int = 90) -> Optional[Dict]:
        key_data = self.keys.get(key_id)
        if not key_data:
            return None
        self.revoke_key(key_id)
        return self.store_key(
            key_data["client_id"],
            key_data["key_id"],
            APIKeyGenerator.generate_key()[1],
            ttl_days,
            key_data.get("rate_limit", 1000)
        )

store = APIKeyStore()
result = store.store_key("scan-service", "key-1",
                         "dag_a1b2c3d4e5f6", rate_limit=500)
print(f"Store result: {result['message']}")
valid, info = store.validate_key("dag_a1b2c3d4e5f6")
print(f"Valid: {valid}, info: {info}")
store.revoke_key("key-1")
valid, info = store.validate_key("dag_a1b2c3d4e5f6")
print(f"After revoke - Valid: {valid}")

Per-Key Rate Limiting

Combine API key authentication with rate limiting for fine-grained access control.

from collections import defaultdict
import time
from typing import Dict, Tuple

class KeyRateLimiter:
    def __init__(self):
        self.usage: Dict[str, list] = defaultdict(list)

    def check_and_record(self, key_id: str,
                         limit: int,
                         window: int = 3600
                         ) -> Tuple[bool, int]:
        now = time.time()
        cutoff = now - window
        self.usage[key_id] = [
            t for t in self.usage[key_id] if t > cutoff
        ]
        if len(self.usage[key_id]) >= limit:
            return False, 0
        self.usage[key_id].append(now)
        remaining = limit - len(self.usage[key_id])
        return True, remaining

    def get_usage_stats(self, key_id: str,
                        window: int = 3600) -> Dict:
        now = time.time()
        cutoff = now - window
        recent = [
            t for t in self.usage.get(key_id, [])
            if t > cutoff
        ]
        return {
            "key_id": key_id,
            "requests_in_window": len(recent),
            "window_seconds": window
        }

limiter = KeyRateLimiter()
for _ in range(3):
    allowed, remaining = limiter.check_and_record("key-1", 5, 60)
    print(f"Allowed: {allowed}, remaining: {remaining}")
stats = limiter.get_usage_stats("key-1", 60)
print(f"Stats: {stats}")

Common Mistakes

Mistake 1: Storing Keys in Plain Text

Always hash API keys before storing. If the database is breached, hashed keys cannot be used directly.

Mistake 2: Showing the Full Key After Creation

Show the raw key once during creation, then only show a truncated form. The gateway should not be able to recover the original key.

Mistake 3: No Key Expiration

Keys that never expire are a security risk. Enforce maximum key lifetimes and require rotation.

Mistake 4: Rate Limiting by IP Instead of Key

Multiple clients behind a NAT share an IP. Always rate limit by API key, not client IP.

Mistake 5: Transmitting Keys in URLs

API keys in URLs appear in server logs and browser history. Require keys in the Authorization header.

Practice Questions

  1. Why should API keys be hashed before storage?
  2. What is the recommended key format for API keys?
  3. How does API key rotation work without breaking production?
  4. Why should API keys be in the Authorization header rather than the URL?
  5. How do you handle key revocation in a distributed gateway?

Challenge

Build an API key authentication system for a gateway that generates secure keys, stores them hashed, supports key rotation with a grace period where both old and new keys are valid, and rate-limits per key to 100 requests per minute.

FAQ

What is the difference between an API key and a JWT?

API keys are static tokens that identify the client. JWTs are structured tokens that contain claims and can encode both identity and permissions.

How long should an API key be?

At least 32 bytes (64 hex characters). Longer keys provide more entropy and are harder to brute force.

Should API keys expire?

Yes. Set keys to expire every 90 days by default. Provide a grace period where both old and new keys are valid during rotation.

How do you secure API key transmission?

Always transmit API keys over HTTPS. Never include them in URLs. Require the Authorization: Bearer header for key transmission.

Can API keys be used for human users?

API keys are best for machine-to-machine communication. For human users, use JWT or OAuth2 which support richer claims and expiration.

Mini Project

Build a complete API key management system for the gateway that generates secure keys with a dag_ prefix, stores SHA-256 hashes, supports list, revoke, and rotate operations, enforces per-key rate limits, and provides a validation endpoint.

What's Next

Learn about JWT Gateway for structured token authentication, or explore OAuth2 Gateway for delegated authorization flows.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro