Revoking and Rotating Portal API Tokens

Machine identities — CI jobs, sync agents, dashboards — authenticate to the portal with long-lived API tokens, and unlike a user session there is no interactive refresh to bound their lifetime, so revocation and scheduled rotation are the only controls that keep a leaked token from being useful forever. This how-to issues scoped, hashed API tokens, revokes them instantly, and rotates them with a dual-token overlap so no automation breaks. It is the machine-identity half of Session & Token Management within Authentication, RBAC & Security Governance.

Prerequisites

  • A portal backend able to mint and store API tokens, or an external authorization server you can query for introspection.
  • A datastore (Postgres or Redis) holding a hash of each token, its scope, and a revoked flag — never the raw token.
  • Scopes defined per consumer, aligned with your Role-Based Access Control Setup, so a token can do only what its job needs.
  • Secrets injected at runtime; a token is shown once at creation and never stored in plaintext.
Prerequisites for revocable API tokens A minting backend, a hashed token store, per-consumer scopes, and runtime injection make tokens revocable and rotatable. minting backend hashed token store per-consumer scopes runtime injection Revocable tokens scoped + rotatable
Storing only a hash means a database leak never exposes usable tokens, and revocation is a single flag flip.

Exact Configuration

  1. Mint a scoped token and store its hash. Return the raw token to the caller once; persist only a hash plus scope and expiry.

    // src/tokens/mint.ts
    // Requires Node.js >= 20
    import { randomBytes, createHash } from 'node:crypto';
    
    export async function mintToken(db: Db, consumer: string, scopes: string[]) {
      const raw = `pt_${randomBytes(24).toString('hex')}`;
      const hash = createHash('sha256').update(raw).digest('hex');
      await db.tokens.insert({
        hash, consumer, scopes, revoked: false,
        expiresAt: Date.now() + 90 * 24 * 3600 * 1000, // 90-day max
      });
      return raw; // shown once, never stored raw
    }
    
  2. Validate by hash and check revocation on every request. A revoked or expired token is rejected regardless of its printed lifetime.

    // src/tokens/validate.ts
    export async function validate(db: Db, raw: string, needed: string) {
      const hash = createHash('sha256').update(raw).digest('hex');
      const t = await db.tokens.findByHash(hash);
      if (!t || t.revoked || t.expiresAt < Date.now()) return false;
      return t.scopes.includes(needed);
    }
    
  3. Revoke instantly by setting the flag; the next request fails closed.

    # Requires the admin API
    curl -s -X POST ${PORTAL_API_URL}/api/tokens/${TOKEN_ID}/revoke \
      -H "Authorization: Bearer ${ADMIN_TOKEN}"
    
  4. Rotate with a dual-token overlap. Issue the new token, deploy it to the consumer, then revoke the old one only after the consumer is confirmed on the new value.

Dual-token overlap rotation A new token is issued and deployed while the old one stays valid, then the old token is revoked after the consumer migrates. Issue new Deploy to consumer Revoke old overlap — both tokens valid, no downtime
Revoking the old token only after the consumer is confirmed on the new one is what makes rotation zero-downtime.

Validation

# Requires curl
# 1. A revoked token is rejected
curl -s -o /dev/null -w "%{http_code}\n" ${PORTAL_API_URL}/api/catalog/entities \
  -H "Authorization: Bearer ${REVOKED_TOKEN}"
# Expected: 401

# 2. A token cannot exceed its scope
curl -s -o /dev/null -w "%{http_code}\n" -X POST ${PORTAL_API_URL}/api/catalog/locations \
  -H "Authorization: Bearer ${READ_ONLY_TOKEN}"
# Expected: 403

# 3. During overlap, both old and new tokens work
for t in "${OLD_TOKEN}" "${NEW_TOKEN}"; do
  curl -s -o /dev/null -w "%{http_code}\n" ${PORTAL_API_URL}/api/catalog/entities \
    -H "Authorization: Bearer $t"
done
# Expected: 200 and 200
Token validation checks A revoked token is rejected, an over-scope request is denied, and both tokens work during overlap. Revoked → 401 fails closed Over-scope → 403 least privilege Overlap → both 200 no downtime
The over-scope 403 proves scopes are enforced, not merely recorded — a token with read scope must never write.

Edge Cases & Troubleshooting

Symptom Root Cause Resolution
Revoked token still works Validation reads a cached copy without the revoked flag Check the revocation flag on every request, or bust the cache on revoke
Rotation breaks a CI job Old token revoked before the consumer redeployed Keep the overlap; revoke only after confirming the new token is in use
Raw tokens found in the database Storing the token instead of its hash Store only the SHA-256 hash; the raw value is shown once
A leaked token has broad access Token minted with wildcard scope Mint least-privilege scopes per consumer; never issue admin scope to automation
No idea which token a call used No token id in audit logs Log the token id (not the value) on every authenticated action
API-token pitfalls by class Stale revocation and broken rotation are lifecycle issues; stored raw tokens and broad scope are storage and scoping issues. Lifecycle stale revocation broken rotation → check flag + overlap Storage & scope stored raw broad scope → hash + least privilege
Hashing tokens and scoping them narrowly limits both what a leak exposes and what it can do.

Frequently Asked Questions

How long should a machine token live?

Bounded — 30 to 90 days — with scheduled rotation before expiry, rather than non-expiring. A never-expiring token is a permanent liability; a bounded one forces a rotation cadence that limits the window any single value is valid, while the overlap keeps rotation from causing outages.

Should automation use API tokens or OIDC client credentials?

Prefer OIDC client-credentials or workload-identity federation where the provider supports it, since those issue short-lived tokens with no static secret to leak. Reserve minted API tokens for consumers that cannot speak OIDC, and give those the same revocation, hashing, and rotation discipline.