API authentication: keys, OAuth2 & JWT
Authentication answers "who is calling?"; authorisation answers "what may they do?". Different API scenarios call for different mechanisms, and choosing the wrong one creates security holes.
API keys
A long random string that identifies a calling application, sent in a header.
curl https://api.example.in/v1/rates -H "X-API-Key: $RATES_API_KEY"
API keys are simple and ideal for server-to-server calls. They usually identify a project, not a user, and they do not expire on their own. Store only a hash of each key on the server, allow rotation, and never embed keys in a mobile app or front-end JavaScript where anyone can read them.
OAuth 2.0
OAuth2 lets a user grant a third-party app limited access to their data without sharing their password — the "Sign in with Google" and "Connect your bank account" experiences.
| Flow (grant type) | When to use |
|---|---|
| Authorization Code + PKCE | Web, mobile and single-page apps acting for a user |
| Client Credentials | Machine-to-machine, no user involved |
| Refresh Token | Get a new access token without asking the user again |
| Implicit, Password | Legacy — avoid in new systems |
The Authorization Code flow in brief:
- Your app redirects the user to the provider's login page with
client_id,redirect_uri,scopeand a PKCE challenge. - The user logs in and approves the requested scopes.
- The provider redirects back with a short-lived
code. - Your server exchanges the code (plus the PKCE verifier) for an access token and often a refresh token.
- You call the API with
Authorization: Bearer <access_token>.
Scopes such as orders:read limit what the token can do. OpenID Connect adds an ID token on top of OAuth2 to tell
you who the user is.
JSON Web Tokens (JWT)
A JWT is a compact, signed token with three Base64URL parts: header.payload.signature. The payload holds claims:
{"sub": "user_42", "role": "manager", "iat": 1790000000, "exp": 1790000900}
The signature lets any service verify the token without a database lookup. Note the payload is only encoded, not encrypted, so never put passwords or Aadhaar numbers in it.
import datetime as dt
import jwt # pip install PyJWT
SECRET = "load-me-from-an-environment-variable"
def issue_token(user_id: int) -> str:
now = dt.datetime.now(dt.timezone.utc)
payload = {"sub": str(user_id), "iat": now, "exp": now + dt.timedelta(minutes=15)}
return jwt.encode(payload, SECRET, algorithm="HS256")
def verify_token(token: str) -> dict:
# Raises jwt.ExpiredSignatureError or jwt.InvalidTokenError on failure
return jwt.decode(token, SECRET, algorithms=["HS256"])
Always pass an explicit algorithms list when decoding, so an attacker cannot switch the algorithm.
Comparison
| API key | OAuth2 access token | Session cookie | |
|---|---|---|---|
| Identifies | Application | User + app + scopes | Browser user |
| Expiry | Manual rotation | Minutes to hours | Configurable |
| Best for | Server-to-server | Third-party and mobile apps | Traditional web apps |
Common mistakes
- Sending credentials over plain HTTP instead of HTTPS.
- Long-lived JWTs with no way to revoke them; keep access tokens short and use refresh tokens.
- Storing tokens in
localStoragewhere any injected script can steal them. - Committing keys to Git — use environment variables or a secrets manager, and rotate immediately if leaked.
Practice
Write two small Python functions using PyJWT: one that issues a 10-minute token with a role claim, and one that
verifies it and returns 401-style errors for expired or tampered tokens. Test by editing one character of the token.