TokenExpiredError 🔐 Authentication

JWT Expired / Invalid Token

The JSON Web Token sent with the request is expired, malformed or signed with a different key, so the API rejects it (usually with 401).

Seen on: Node.js PHP Python REST API

Meaning

JWTs carry an exp (expiry) claim; once the current time passes it, every API that validates the token rejects it. “Invalid token” variants mean the signature doesn’t verify (wrong secret/key, wrong algorithm), the token was truncated, or claims like aud/iss don’t match.

Common causes

  • Access token passed its exp time and wasn’t refreshed
  • Server clock skew (token looks expired or “not before” nbf)
  • Token signed with a different secret/key than the verifier uses (e.g. different environments)
  • Algorithm mismatch (HS256 vs RS256) or rotated keys/JWKS cache
  • Token truncated or with extra quotes/whitespace (copied from logs)
  • aud / iss claims don’t match what the API expects

⚡ Quick fix

  1. Decode the token (header + payload) and check exp, iss, aud, alg
  2. Implement refresh: on 401, use the refresh token to get a new access token, then retry once
  3. Sync server clocks (NTP) and allow a small leeway (30–60s)
  4. Make sure issuer and verifier share the same secret/public key

Detailed fix by platform

Node.js

  1. jsonwebtoken with clock tolerance and explicit algorithms:
    javascript
    import jwt from 'jsonwebtoken';
    try {
      const claims = jwt.verify(token, publicKey, { algorithms: ['RS256'], audience: 'my-api', clockTolerance: 30 });
    } catch (err) {
      if (err.name === 'TokenExpiredError') return res.status(401).json({ error: 'token_expired' });
      return res.status(401).json({ error: 'invalid_token' });
    }

PHP

  1. firebase/php-jwt: set JWT::$leeway = 30; and pass the correct new Key($key, 'RS256'). Catch ExpiredException separately to trigger refresh.

Python

  1. PyJWT: jwt.decode(token, key, algorithms=["RS256"], audience="my-api", leeway=30); handle jwt.ExpiredSignatureError.

Code examples

Decode a JWT payload locally (no verification)

javascript
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
console.log(new Date(payload.exp * 1000), payload.aud, payload.iss);

Never trust a decoded-but-unverified token on the server — this is only for debugging.

How to diagnose

  1. Token present — Is the full token sent as Bearer <token>?
  2. Claims — exp / nbf / iss / aud correct?
  3. Signature — Same key and algorithm on issuer and verifier?
  4. Clock — Are server clocks in sync?
  5. Refresh — Does the client refresh before/after expiry?

🧠 Still stuck? Analyze your error

Paste the full message, response headers or stack trace — we'll detect the platform and point to the most likely cause.