Authentication and Tokens
What You'll Learn
The authentication methods you'll encounter in real APIs, how to implement each one correctly, and how to handle token refresh and expiration.
API Key Authentication
The simplest form — pass a key in the header or query string:
import os
import requests
API_KEY = os.environ["MY_API_KEY"]
# In Authorization header (preferred)
response = requests.get(
"https://api.example.com/data",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
# As X-API-Key header (also common)
response = requests.get(
"https://api.example.com/data",
headers={"X-API-Key": API_KEY},
timeout=10,
)
# As query parameter (less secure — appears in logs)
response = requests.get(
"https://api.example.com/data",
params={"api_key": API_KEY},
timeout=10,
)
HTTP Basic Auth
Username and password sent with every request:
import requests
import os
response = requests.get(
"https://api.example.com/data",
auth=(os.environ["API_USER"], os.environ["API_PASS"]),
timeout=10,
)
Never hardcode credentials. Always read from environment variables.
Bearer Token (JWT)
Many modern APIs use short-lived tokens that you exchange a longer-lived credential for:
import os
import requests
from datetime import datetime, timedelta
class TokenClient:
"""API client that manages Bearer token authentication."""
def __init__(self, auth_url: str, client_id: str, client_secret: str):
self.auth_url = auth_url
self.client_id = client_id
self.client_secret = client_secret
self._token: str | None = None
self._token_expires: datetime = datetime.min
def _get_token(self) -> str:
"""Fetch a new access token from the auth server."""
response = requests.post(
self.auth_url,
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
},
timeout=10,
)
response.raise_for_status()
data = response.json()
self._token = data["access_token"]
expires_in = data.get("expires_in", 3600)
# Renew 60 seconds before actual expiry
self._token_expires = datetime.utcnow() + timedelta(seconds=expires_in - 60)
return self._token
@property
def token(self) -> str:
if self._token is None or datetime.utcnow() >= self._token_expires:
return self._get_token()
return self._token
def get(self, url: str) -> dict:
response = requests.get(
url,
headers={"Authorization": f"Bearer {self.token}"},
timeout=10,
)
response.raise_for_status()
return response.json()
# Usage
client = TokenClient(
auth_url=os.environ["AUTH_URL"],
client_id=os.environ["CLIENT_ID"],
client_secret=os.environ["CLIENT_SECRET"],
)
data = client.get("https://api.example.com/resources")
OAuth2 — User Delegated Access
OAuth2 lets users grant your app access to their accounts. The flow:
1. Redirect user to: /oauth/authorize?client_id=...&redirect_uri=...&scope=...
2. User logs in and approves
3. Provider redirects back: /callback?code=abc123
4. Exchange code for tokens: POST /oauth/token {code: "abc123", ...}
5. Use access_token for API calls
6. Use refresh_token to get new access_token when expired
Simple implementation with Flask:
from flask import Flask, redirect, request, session
import requests
import os
import secrets
app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET"]
CLIENT_ID = os.environ["GITHUB_CLIENT_ID"]
CLIENT_SECRET = os.environ["GITHUB_CLIENT_SECRET"]
REDIRECT_URI = "http://localhost:5000/callback"
@app.route("/login")
def login():
state = secrets.token_urlsafe(16)
session["oauth_state"] = state
return redirect(
f"https://github.com/login/oauth/authorize"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&scope=repo"
f"&state={state}"
)
@app.route("/callback")
def callback():
if request.args.get("state") != session.pop("oauth_state", None):
return "State mismatch — possible CSRF", 400
code = request.args["code"]
response = requests.post(
"https://github.com/login/oauth/access_token",
json={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code": code,
"redirect_uri": REDIRECT_URI,
},
headers={"Accept": "application/json"},
timeout=10,
)
tokens = response.json()
session["access_token"] = tokens["access_token"]
return redirect("/dashboard")
Securely Storing and Loading Credentials
import os
import sys
# ❌ NEVER hardcode credentials
API_KEY = "sk-abc123xyz" # will end up in git history
# ✅ Read from environment
API_KEY = os.environ.get("API_KEY")
# ✅ Fail fast if missing
def require_env(key: str) -> str:
value = os.environ.get(key)
if not value:
print(f"Error: {key} environment variable is required", file=sys.stderr)
sys.exit(1)
return value
API_KEY = require_env("API_KEY")
# ✅ Use .env for local dev
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.environ["API_KEY"]
Handling 401 / Token Expiry
class APIClient:
def _request_with_retry(self, method: str, url: str) -> dict:
response = self.session.request(method, url, timeout=10)
if response.status_code == 401:
# Token expired — refresh and retry once
self._refresh_token()
response = self.session.request(method, url, timeout=10)
response.raise_for_status()
return response.json()
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Hardcoded credentials | Exposed in git history forever | Use environment variables |
| Token in query string | Appears in server logs and URLs | Use Authorization header |
| No token expiry handling | Requests fail after token expires | Check and refresh before expiry |
| No state parameter in OAuth | Vulnerable to CSRF | Always verify state |
| Logging full Authorization header | Exposes token in logs | Never log auth headers |
Quick Reference
# API Key (header)
requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
# Basic auth
requests.get(url, auth=(user, password))
# Token with auto-refresh
@property
def token(self) -> str:
if expired:
self._refresh()
return self._token
# OAuth2 — exchange code for token
requests.post(token_url, json={
"grant_type": "authorization_code",
"code": code,
"client_id": client_id,
"client_secret": client_secret,
})
# Env vars
api_key = os.environ["API_KEY"]
from dotenv import load_dotenv; load_dotenv()