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:

  1. Your app redirects the user to the provider's login page with client_id, redirect_uri, scope and a PKCE challenge.
  2. The user logs in and approves the requested scopes.
  3. The provider redirects back with a short-lived code.
  4. Your server exchanges the code (plus the PKCE verifier) for an access token and often a refresh token.
  5. 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 localStorage where 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.

B

Written by Bablu Kumar Jha

Founder, BeeProton AI · Mumbai, Maharashtra, India