Skip to main content

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

MistakeConsequenceFix
Hardcoded credentialsExposed in git history foreverUse environment variables
Token in query stringAppears in server logs and URLsUse Authorization header
No token expiry handlingRequests fail after token expiresCheck and refresh before expiry
No state parameter in OAuthVulnerable to CSRFAlways verify state
Logging full Authorization headerExposes token in logsNever 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()

What's Next

Lesson 5: Pagination and Rate Limits