Skip to content

API Auth — API Keys, JWT & OAuth2 Client Credentials

DodaTech Updated 2026-06-24 4 min read

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

API authentication validates client identity using API keys, JWT tokens, or OAuth2 Client Credentials — each suited to different trust levels and security requirements.

What You'll Learn

You will learn how to implement three API authentication methods — API keys, JWT bearer tokens, and OAuth2 Client Credentials flow — and when to choose each.

Why Authentication Matters

An unauthenticated API is a public endpoint anyone can call. Authentication ensures only authorized clients access your data. DodaTech's Durga Antivirus Pro partner API authenticates 5,000+ integrations — a compromised key could expose threat intelligence to competitors.

Method 1: API Keys

Simple, static tokens passed in headers or query parameters.

from flask import Flask, jsonify, request, abort

app = Flask(__name__)

API_KEYS = {
    "dodatech_partner_key_abc123": {"client": "PartnerCorp", "tier": "premium"},
    "dodatech_partner_key_def456": {"client": "StartupInc", "tier": "basic"},
}

def authenticate_api_key():
    api_key = request.headers.get("X-API-Key")
    if not api_key:
        api_key = request.args.get("api_key")

    if not api_key or api_key not in API_KEYS:
        abort(401, description="Invalid or missing API key")

    return API_KEYS[api_key]

@app.route("/v2/threats")
def list_threats():
    client = authenticate_api_key()
    return jsonify({
        "client": client["client"],
        "tier": client["tier"],
        "threats": fetch_threats(tier=client["tier"]),
    })

Expected output:

curl -H "X-API-Key: dodatech_partner_key_abc123" \
  http://localhost:5000/v2/threats
# {"client": "PartnerCorp", "tier": "premium", "threats": [...]}

curl http://localhost:5000/v2/threats
# HTTP/1.1 401 UNAUTHORIZED
# {"error": "Invalid or missing API key"}

Method 2: JWT Bearer Tokens

Stateless tokens with encoded claims and expiration.

import jwt
from datetime import datetime, timedelta, timezone

SECRET_KEY = "dodatech-jwt-secret-key-change-in-production"

def create_jwt(client_id, tier):
    payload = {
        "sub": client_id,
        "tier": tier,
        "iat": datetime.now(timezone.utc),
        "exp": datetime.now(timezone.utc) + timedelta(hours=1),
    }
    return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

def verify_jwt():
    token = request.headers.get("Authorization", "").removeprefix("Bearer ")
    if not token:
        abort(401, description="Missing Authorization header")

    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        return payload
    except jwt.ExpiredSignatureError:
        abort(401, description="Token expired")
    except jwt.InvalidTokenError:
        abort(401, description="Invalid token")

@app.route("/v2/auth/login", methods=["POST"])
def login():
    data = request.get_json()
    if data.get("api_key") in API_KEYS:
        client = API_KEYS[data["api_key"]]
        token = create_jwt(client["client"], client["tier"])
        return jsonify({"access_token": token, "token_type": "Bearer"})
    abort(401, description="Invalid credentials")

@app.route("/v2/threats/scan")
def scan_threat():
    payload = verify_jwt()
    return jsonify({
        "client": payload["sub"],
        "scan_result": simulate_scan(),
    })

Expected output:

curl -X POST http://localhost:5000/v2/auth/login \
  -H "Content-Type: application/json" \
  -d '{"api_key": "dodatech_partner_key_abc123"}'
# {"access_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "Bearer"}

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  http://localhost:5000/v2/threats/scan
# {"client": "PartnerCorp", "scan_result": "clean"}

Method 3: OAuth2 Client Credentials

import httpx

# OAuth2 token endpoint (simulated auth server)
AUTH_SERVER = "https://auth.dodatech.com/oauth/token"
CLIENT_ID = "dodatech-api"
CLIENT_SECRET = "client-secret-change-me"

def get_client_credentials_token():
    response = httpx.post(
        AUTH_SERVER,
        data={
            "grant_type": "client_credentials",
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "scope": "threats:read threats:write",
        },
    )
    response.raise_for_status()
    return response.json()

# Token introspection
def introspect_token(token):
    response = httpx.post(
        f"{AUTH_SERVER}/introspect",
        data={"token": token},
        auth=(CLIENT_ID, CLIENT_SECRET),
    )
    return response.json()

@app.route("/v2/oauth/threats")
def oauth_threats():
    auth_header = request.headers.get("Authorization", "")
    token = auth_header.removeprefix("Bearer ")

    introspection = introspect_token(token)
    if not introspection.get("active"):
        abort(401, description="Token is invalid or expired")

    return jsonify({
        "client_id": introspection.get("client_id"),
        "scope": introspection.get("scope"),
        "threats": fetch_threats(),
    })

Expected output:

# Get token from auth server
curl -X POST https://auth.dodatech.com/oauth/token \
  -u "dodatech-api:client-secret-change-me" \
  -d "grant_type=client_credentials&scope=threats:read"
# {"access_token": "eyJraWQiOiJ...", "expires_in": 3600, "scope": "threats:read"}

# Use token to access API
curl -H "Authorization: Bearer eyJraWQiOiJ..." \
  http://localhost:5000/v2/oauth/threats
# {"client_id": "dodatech-api", "scope": "threats:read", "threats": [...]}
flowchart TD
    A["Choose Auth Method"] --> B{"Who calls\nthe API?"}
    B -->|"Server-to-Server"| C["OAuth2 Client Credentials\nBest for machine-to-machine"]
    B -->|"First-party clients"| D["API Keys\nSimple, static tokens"]
    B -->|"Third-party apps"| E["OAuth2 Authorization Code\nDelegated user auth"]
    B -->|"SPA / Mobile"| F["JWT Bearer Tokens\nStateless sessions"]
    C --> G["+ Scoped permissions"]
    C --> H["- Requires auth server"]
    D --> I["+ Simple to implement"]
    D --> J["- Hard to revoke per-client"]
    E --> K["+ User consent flow"]
    F --> L["+ No server-side state"]
    style A fill:#dbeafe,stroke:#2563eb
    style C fill:#bbf7d0,stroke:#16a34a
    style D fill:#fef3c7,stroke:#d97706
    style F fill:#e0e7ff,stroke:#4f46e5

Common Errors

1. Storing Secrets in Code

Hardcoded API keys and JWT secrets in source code are exposed in Git history. Use environment variables or a secrets manager.

2. JWT Without Expiration

Tokens without exp never expire. If a token leaks, it is valid forever. Always set short expiration (15-60 minutes) and use refresh tokens for longer sessions.

3. Not Validating Token Scope

A token issued for threats:read should not access threats:write. Always validate the scope claim against the requested operation.

4. API Key Leakage in URLs

Passing API keys as query parameters leaks them in server logs, referrer headers, and browser history. Use the Authorization header instead.

5. Missing Rate Limits on Auth Endpoints

The login endpoint is the most attacked endpoint. Rate limit it aggressively (5 attempts per minute per IP) and implement account lockout.

Practice Questions

1. What is the difference between authentication and authorization?

Authentication verifies identity ("who you are"). Authorization determines permissions ("what you can do").

2. When would you use OAuth2 Client Credentials vs JWT?

Client Credentials when you need delegated authorization with scope and an auth server. JWT when you want self-contained stateless tokens without an external auth server.

3. How do you revoke a JWT before it expires?

Maintain a denylist of revoked JWT IDs (jti) on the server, or use short-lived tokens with refresh tokens that can be revoked individually.

4. Challenge: Implement token refresh for the JWT auth system.

REFRESH_TOKENS = {}

def create_refresh_token(client_id):
    token = secrets.token_urlsafe(32)
    REFRESH_TOKENS[token] = client_id
    return token

@app.route("/v2/auth/refresh", methods=["POST"])
def refresh():
    refresh_token = request.get_JSON().get("refresh_token")
    client_id = REFRESH_TOKENS.pop(refresh_token, None)
    if not client_id:
        abort(401, description="Invalid refresh token")
    new_token = create_JWT(client_id, "premium")
    new_refresh = create_refresh_token(client_id)
    return jsonify({"access_token": new_token, "refresh_token": new_refresh})

Mini Project: Auth Middleware

Build an authentication middleware for DodaTech's threat API that supports API key auth (legacy partners) and JWT auth (new partners). Include middleware that checks auth on every endpoint and logs which client accessed which endpoint.

Related Tutorials

JWTOAuth 2.0RESTful API Design


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-24.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro