Skip to content

Tokens, JWT & OAuth

Most APIs authenticate with bearer tokens: a credential the client presents on each request. JWTs are a popular token format, and OAuth2 is the protocol for obtaining tokens on a user’s behalf.

Authorization: Bearer <token>

“Bearer” means whoever holds the token can use it — so tokens must travel over HTTPS, be short-lived, and be revocable.

A JSON Web Token has three base64url parts separated by dots: header.payload.signature. The header names the algorithm, the payload holds claims (such as sub, exp, aud, iss), and the signature is computed by the issuer over the first two parts.

JavaScript

Anyone can read a JWT payload — it is not encrypted. Trust comes only from verifying the signature with the issuer’s key, and checking exp (not expired), aud (intended for your API), and iss (from a trusted issuer). Never trust claims you have not verified.

OAuth2 lets a user grant an app limited access without sharing their password. The dominant flow:

sequenceDiagram
  participant U as User
  participant App as Client App
  participant AS as Authorization Server
  participant API as Resource API
  U->>App: use the app
  App->>AS: redirect to authorize (scopes)
  U->>AS: log in + consent
  AS-->>App: authorization code
  App->>AS: exchange code (+ secret) for tokens
  AS-->>App: access token (+ refresh token)
  App->>API: request + Bearer access token
  API-->>App: protected resource
The OAuth2 authorization-code flow (OIDC adds an identity token)

Scopes limit what a token can do (articles:read, articles:write); your API checks the token’s scopes during authorization. OpenID Connect (OIDC) layers identity (an id_token) on top of OAuth2.

Token TypeAdvantagesDisadvantagesFits
JWT (stateless)No database lookup per requestHard to revoke — must wait for expiryHigh-scale APIs, microservices
Opaque token (stateful)Revokes instantlyNeeds a database lookup per requestSessions that need frequent revocation
API keySimple, long-livedNo expiry, hard to revokeServer-to-server, developer APIs
OAuth access tokenDelegated auth, scopedMore setup complexityThird-party integrations

Storing a JWT in localStorage Symptom:

  • localStorage.setItem('token', jwt) in the browser
  • An XSS attack can steal the token
  • Store it in an httpOnly cookie instead — JavaScript can’t read it

A JWT that never expires Symptom:

  • jwt.sign(payload, secret) with no expiresIn
  • A stolen token stays valid forever
  • Set expiresIn: '1h' and add a refresh token mechanism

💡 Real-world examples

GitHub API:

  • Personal access tokens (opaque) — revocable instantly from settings
  • GitHub Apps use a JWT for installation tokens that expire after 1 hour

Stripe API:

  • An API key for server-to-server use — never use it in client-side code
  • Restricted keys that limit permissions to only what’s needed

Sources: GitHub Apps — authenticating, Stripe API keys

What are the three parts of a JWT?
Why is reading a JWT payload not enough to trust it?
What do OAuth2 scopes control?