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.
Exact Configuration
-
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 } -
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); } -
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}" -
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.
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
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 |
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.