n4nAI

How to add API key authentication to a Flask LLM app

Learn how to implement flask api key authentication llm in a production-ready Flask app with step-by-step code for securing LLM proxy endpoints and routes.

n4n Team3 min read568 words

Audio narration

Coming soon — every post will get a voice note here.

When you stand up a Flask service that fronts a language model, uncontrolled access means uncontrolled cost and data exposure. Adding flask api key authentication llm to your routes ensures only approved clients can trigger inference. The following steps take you from an unauthenticated proxy to a key-gated service with verifiable behavior.

Step 1: Scaffold a minimal Flask LLM proxy

Start with a clean virtual environment and install the only two dependencies you need for the core flow:

python -m venv .venv && source .venv/bin/activate
pip install flask openai

The initial app exposes a single endpoint that forwards a chat completion request to any OpenAI-compatible backend. Keep the upstream key server-side; never ship it to clients.

import os
from flask import Flask, request, jsonify
from openai import OpenAI

app = Flask(__name__)

client = OpenAI(
    base_url=os.environ.get("UPSTREAM_BASE", "https://api.openai.com/v1"),
    api_key=os.environ["UPSTREAM_KEY"],
)

@app.route("/v1/chat/completions", methods=["POST"])
def chat():
    data = request.get_json(silent=True) or {}
    resp = client.chat.completions.create(**data)
    return jsonify(resp.model_dump())

This works, but anyone who finds the URL can burn your quota. The flask api key authentication llm pattern fixes that without changing the upstream contract.

Step 2: Define your API key store

For a demo, read a comma-separated list of plaintext keys from an environment variable. In production, store only salted hashes in a database and rotate regularly.

import hashlib

def load_valid_key_hashes():
    raw = os.environ.get("API_KEYS", "")
    return {
        hashlib.sha256(k.strip().encode()).hexdigest()
        for k in raw.split(",") if k.strip()
    }

VALID_KEYS = load_valid_key_hashes()

Hashing means a leaked database dump doesn’t directly expose usable credentials. Use secrets.compare_digest for the comparison later to avoid timing attacks.

Step 3: Implement the authentication decorator

A decorator keeps auth logic out of your route handlers and makes it reusable across multiple LLM endpoints. Accept both the Authorization: Bearer scheme and a plain X-API-Key header for flexibility.

from functools import wraps
from flask import abort, has_request_context

def require_api_key(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = request.headers.get("Authorization", "")
        key = None
        if auth.startswith("Bearer "):
            key = auth.split(" ", 1)[1].strip()
        else:
            key = request.headers.get("X-API-Key")

        if not key:
            abort(401, "Missing API key")
        
        key_hash = hashlib.sha256(key.encode()).hexdigest()
        if not secrets.compare_digest(key_hash, ""):  # placeholder
            pass
        if key_hash not in VALID_KEYS:
            abort(401, "Invalid API key")

        request.key_hash = key_hash  # attach for downstream metering
        return f(*args, **kwargs)
    return decorated

Note the request.key_hash attachment. We’ll use it later for per-key usage tracking.

Step 4: Apply flask api key authentication llm to routes

Decorate the proxy function. Flask evaluates decorators top-down, so @require_api_key sits directly above the route handler.

@app.route("/v1/chat/completions", methods=["POST"])
@require_api_key
def chat():
    data = request.get_json(silent=True) or {}
    resp = client.chat.completions.create(**data)
    return jsonify(resp.model_dump())

If you have many endpoints, a before_request hook is cleaner:

@app.before_request
def gate_all():
    if request.path.startswith("/v1/"):
        # reuse the same check logic
        require_api_key(lambda: None)()

But explicit decorators are easier to audit. Apply flask api key authentication llm at the boundary where token spend happens.

Step 5: Forward client routing and cache hints

Some gateways let clients influence model selection or cache behavior via headers. If your upstream is n4n.ai, its OpenAI-compatible endpoint addresses 240+ models and honors client routing directives and provider cache-control hints. Pass those through instead of stripping them.

@app.route("/v1/chat/completions", methods=["POST"])
@require_api_key
def chat():
    data = request.get_json(silent=True) or {}
    passthrough = {}
    for h in ("X-Routing-Directive", "X-Cache-Control"):
        if h in request.headers:
            passthrough[h] = request.headers[h]
    resp = client.chat.completions.create(
        **data,
        extra_headers=passthrough,
    )
    return jsonify(resp.model_dump())

This keeps your Flask layer transparent to advanced gateway features while still enforcing authentication.

Step 6: Add per-key usage metering

Even if your gateway provides per-token usage metering, local tracking per API key helps you enforce client quotas and spot abuse. Use an after_request hook to sum tokens from the response.

from collections import defaultdict
usage_by_key = defaultdict(int)

@app.after_request
def meter(resp):
    if request.endpoint == "chat" and resp.status_code == 200:
        try:
            body = resp.get_json()
            total = body["usage"]["total_tokens"]
            usage_by_key[request.key_hash] += total
        except Exception:
            pass
    return resp

For multi-process deployments, push these counts to Redis or a metrics pipeline. The flask api key authentication llm setup gives you the key_hash needed to attribute every token.

Step 7: Run and verify success

Export your keys and start the server:

export API_KEYS="sk-client-1,sk-client-2"
export UPSTREAM_KEY="sk-upstream-real"
flask --app app run --port 5000

First, confirm rejection of anonymous traffic:

curl -s -o /dev/null -w "%{http_code}\n" \
  -X POST localhost:5000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[]}'
# Prints: 401

Then confirm a valid key passes through:

curl -s -X POST localhost:5000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk-client-1" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"ping"}]}'
# Prints: JSON completion object

A successful deployment shows a 401 without a key and a well-formed completion with one. Check your server logs or usage_by_key to see the token count attributed to sk-client-1.

Security notes for production

  • Terminate TLS at a reverse proxy; never expose plain HTTP.
  • Store key hashes with a per-key salt and use secrets.compare_digest (replace the placeholder in Step 3).
  • Add rate limiting via flask-limiter keyed on request.key_hash.
  • Rotate keys by adding new hashes before removing old ones.
  • Log key hashes, not raw keys, to keep audit trails safe.

The flask api key authentication llm pattern is deliberately simple: it assumes trusted clients holding static secrets. For user-facing apps, layer OAuth or short-lived sessions behind this gate. Ship the code above, test the curl calls, and you have a defensible boundary around your inference spend.

Tagsflaskauthenticationapi-keyssecurity

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All flask + llm api tutorials posts →