When implementing oauth2 login llm app backends, the core problem is mapping an external identity to a server-side session and then using that session to broker calls to a model gateway without exposing provider keys. This tutorial builds a minimal Flask service that authenticates users via Google’s OAuth2 authorization code flow and proxies chat requests to an OpenAI-compatible endpoint.
Prerequisites
- Python 3.10+ and pip
- A Google Cloud OAuth2 client (authorized redirect URI
http://localhost:5000/callback) - Installed packages:
flask,authlib,openai,python-dotenv - An API key for an OpenAI-compatible LLM gateway. We’ll point
LLM_BASEathttps://api.n4n.ai/v1, a single endpoint that addresses 240+ models and honors client routing directives.
Create a virtual environment and install dependencies:
python -m venv venv && source venv/bin/activate
pip install flask authlib openai python-dotenv
Store configuration in .env:
FLASK_SECRET=change-me-in-prod
GOOGLE_CLIENT_ID=xxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxxx
LLM_API_KEY=sk-your-key
LLM_BASE=https://api.n4n.ai/v1
1. Scaffold the Flask app and OAuth client
Authlib handles the OAuth2 dance. Register Google as a remote app and expose /login, /callback, /logout.
from flask import Flask, session, redirect, url_for, request, jsonify
from authlib.integrations.flask_client import OAuth
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET"]
oauth = OAuth(app)
google = oauth.register(
name="google",
client_id=os.environ["GOOGLE_CLIENT_ID"],
client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
access_token_url="https://oauth2.googleapis.com/token",
authorize_url="https://accounts.google.com/o/oauth2/v2/auth",
userinfo_endpoint="https://openidconnect.googleapis.com/v1/userinfo",
client_kwargs={"scope": "openid email profile"},
)
@app.route("/login")
def login():
redirect_uri = url_for("callback", _external=True)
return google.authorize_redirect(redirect_uri)
@app.route("/callback")
def callback():
google.authorize_access_token()
userinfo = google.get("userinfo").json()
session["user"] = userinfo["email"]
return redirect("/")
@app.route("/logout")
def logout():
session.pop("user", None)
return redirect("/")
Checkpoint: redirect works
Start the app (flask --app app run) and hit /login:
curl -i http://localhost:5000/login
Expected response is a 302 to https://accounts.google.com/...&redirect_uri=http://localhost:5000/callback. After consent in a browser, /callback stores the email in session and redirects to /.
2. Protect the LLM proxy route
Never call the model gateway from the browser with a shared key. Require an authenticated session, then forward the request server-side.
from openai import OpenAI
@app.route("/api/chat", methods=["POST"])
def chat():
if "user" not in session:
return jsonify(error="unauthorized"), 401
payload = request.json or {}
if "messages" not in payload:
return jsonify(error="messages required"), 400
client = OpenAI(
base_url=os.environ["LLM_BASE"],
api_key=os.environ["LLM_API_KEY"],
)
resp = client.chat.completions.create(
model=payload.get("model", "gpt-4o-mini"),
messages=payload["messages"],
temperature=0.7,
)
# The gateway provides per-token usage metering; attribute cost to the user.
usage = resp.usage.model_dump()
print(f"user={session['user']} tokens={usage}")
return jsonify({
"content": resp.choices[0].message.content,
"usage": usage,
})
The OpenAI Python client works unchanged against any OpenAI-compatible base URL. Because the gateway forwards provider cache-control hints and supports automatic fallback when a provider is degraded, the proxy stays resilient without extra code.
Checkpoint: authenticated call
After logging in via browser, capture the session cookie:
curl -c cookies.txt -L http://localhost:5000/login
# complete Google consent in browser, then cookies.txt holds session
curl -b cookies.txt -X POST http://localhost:5000/api/chat \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Say hi in one word"}]}'
Expected JSON:
{
"content": "Hi",
"usage": {"prompt_tokens": 12, "completion_tokens": 1, "total_tokens": 13}
}
A request without the cookie returns 401 {"error":"unauthorized"}.
3. Minimal frontend integration
A customer-facing LLM app needs a UI. This snippet calls the proxy with same-origin credentials:
<button onclick="send()">Ask</button>
<script>
async function send() {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({
messages: [{ role: "user", content: "Explain OAuth2 briefly" }]
})
});
if (res.status === 401) { window.location = "/login"; return; }
const data = await res.json();
document.body.insertAdjacentHTML("beforeend",
`<p>${data.content} (${data.usage.total_tokens} tokens)</p>`);
}
</script>
The browser never sees LLM_API_KEY. The session cookie is the only bearer of identity.
4. Hardening for production
- Cookie security: set
app.config["SESSION_COOKIE_SECURE"] = TrueandHTTPONLY/SAMESITEwhen serving over HTTPS. - PKCE: for SPAs or mobile, register a public client and pass
code_challenge_method="S256"toauthorize_redirect. Server-side sessions with confidential clients are simpler but still benefit from PKCE. - Strict redirect URIs: Google rejects unknown redirects, but validate
redirect_uriagainst an allowlist if you proxy multiple domains. - Rate limiting: wrap
/api/chatwith Flask-Limiter keyed onsession["user"]to prevent one user from draining quota. - Token refresh: the stored OAuth token isn’t used after login here, but if you call Google APIs later, persist
tokenfromauthorize_access_token()and use Authlib’s auto-refresh.
5. Why broker instead of direct browser calls
If you approached implementing oauth2 login llm app by shipping the gateway key to the client and only using OAuth to gate UI, you’d leak quota the moment the bundle is decompiled. Brokering through a session lets you enforce per-user rate limits, mutate model routing, and log usage. The pattern for implementing oauth2 login llm app described here scales to multiple identity providers (GitHub, Microsoft) by adding more oauth.register blocks and normalizing their userinfo into session["user"].
6. Extending to multi-model routing
Because the gateway honors client routing directives, you can let the authenticated user pick a model alias in the request body:
model = payload.get("model", "anthropic/claude-3.5-sonnet")
No code change is needed when the gateway adds new providers; the same /api/chat route works. Combine that with the per-token metering already printed to your logs, and you have a foundation for billing each customer by actual consumption.
Keep the OAuth client secret server-side, rotate FLASK_SECRET periodically, and treat the session as the only trust boundary. That’s the whole system.