hcengineering/platform · error · TokenError

Token revoked

Error message

Token revoked

What it means

verifyToken() rejects an otherwise valid, unexpired token because its apiTokenId (in decoded.extra) is reported as revoked by the registered apiTokenRevocationChecker. Revocation is checked with a short TTL cache; the account service is the sole authority, so a revoked token is refused even if its signature and expiry are still fine. Callers such as the transactor REST API and blob access surface this as a TokenError.

Source

Thrown at foundations/core/packages/token/src/token.ts:227

  }
}

/**
 * Decodes and fully validates a token: signature (via {@link decodeToken}),
 * expiry, and — for revokable API tokens — revocation. Reuse this instead of
 * `decodeToken` anywhere expired or revoked tokens must be rejected (transactor
 * REST API, blob access, etc.) so the policy lives in one place.
 * @public
 */
export async function verifyToken (token: string, secret?: string): Promise<Token> {
  const decoded = decodeToken(token, true, secret)
  if (isTokenExpired(decoded)) {
    throw new TokenError('Token expired')
  }
  const apiTokenId = decoded.extra?.apiTokenId
  if (apiTokenId !== undefined && apiTokenRevocationChecker !== undefined) {
    if (await isApiTokenRevoked(apiTokenId, decoded, token, Date.now())) {
      throw new TokenError('Token revoked')
    }
  }
  return decoded
}

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Obtain a fresh, non-revoked API token from the account service and replace the configured token.
  2. Check the token's apiTokenId against the account service revocation list to confirm why it was revoked.
  3. If the revocation was unintended, re-issue/restore the token in the account service.
  4. If revocation should not apply, note that cache entries within the TTL are trusted — wait out the TTL after un-revoking, then retry.

Example fix

// before
const token = process.env.OLD_API_TOKEN // revoked in account service
await verifyToken(token) // throws TokenError('Token revoked')
// after
const token = process.env.API_TOKEN // freshly issued, unrevoked
await verifyToken(token)
Defensive patterns

Strategy: try-catch

Validate before calling

import { decodeToken, isTokenExpired } from '<token-package>' // pseudo
const decoded = decodeToken(token, true, secret)
if (isTokenExpired(decoded)) throw new Error('expired first')
const apiTokenId = decoded.extra?.apiTokenId
if (apiTokenId !== undefined && !apiTokenId) throw new Error('malformed apiTokenId')

Type guard

function hasApiTokenId(t: unknown): t is Token & { extra: { apiTokenId: string } } {
  return typeof t === 'object' && t !== null &&
    typeof (t as any).extra?.apiTokenId === 'string'
}

Try / catch

try {
  const decoded = await verifyToken(token)
  // use decoded
} catch (e) {
  if (e instanceof TokenError && e.message === 'Token revoked') {
    // prompt re-auth / issue new API token
  } else throw e
}

Prevention

When it happens

Trigger: verifyToken(token) is called; decodeToken succeeds, the token is not expired, decoded.extra.apiTokenId is defined, a revocation checker is registered (setApiTokenRevocationChecker), and the checker (or a fresh cache entry, i.e. outside REVOCATION_CACHE_TTL_MS) reports revoked=true.

Common situations: A user deleted or rotated their API token in the account service while an old token string is still configured in a client; an admin revoked a leaked token; a personal access token was removed after an offboarding.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/78e74c9e0a46d8ae. Report an issue: GitHub.