Quota
/ docs
Dashboard
Docs/Developers/OAuth secret rotation

OAuth secret rotation

Issue a new client_secret for your Sign-in-with-Quota app while the previous one keeps working for 30 days. Roll the new value through CI on your own schedule, then either let the old one expire or revoke it immediately if you suspect compromise.

01 How the grace period works

Each oauth_clients row stores up to two secret hashes:

  • client_secret_hash — the primary (current) secret.
  • client_secret_hash_secondary — the previous secret, kept alive until secondary_expires_at.

At POST /oauth/token the server compares your submitted secret against the primary first; on mismatch, it falls back to the secondary if and only if secondary_expires_at is strictly in the future. Both compares are timing-safe, so an attacker cannot tell from response timing which slot they hit.

Calling POST /developers/apps/:id/rotate-secret performs an atomic swap:

  1. Generate a fresh plaintext secret server-side.
  2. Copy the existing primary hash into the secondary slot, set secondary_expires_at = NOW() + INTERVAL '30 days'.
  3. Store the new hash as primary.
  4. Return the new plaintext to the caller, once.
The plaintext is shown once
The response body is the only place the new client_secret ever appears in plaintext. Store it immediately. There is no recovery endpoint — if you lose it before it lands in your secret store, rotate again.

02 Operational flow

The intended sequence for a non-breaking rotation:

  1. Call POST /developers/apps/:id/rotate-secret and capture the new plaintext.
  2. Update your secret store (Vault, AWS Secrets Manager, .env in CI, etc.) with the new value.
  3. Redeploy every service that uses the old secret. You have 30 days.
  4. Optionally call POST /developers/apps/:id/revoke-secondary-secret once you have confirmed every deployment is on the new secret. If you skip this step, the old secret stops being accepted automatically at secondary_expires_at.
Rotating a second time mid-grace-period
Calling rotate-secret again before the previous grace period ends overwrites the secondary slot. The original secret is immediately invalidated; only the most recent two secrets are ever live. There is no chain of grace periods.

03 Authentication

Both endpoints require a developer session cookie (set by POST /auth/login). Bearer API keys are not accepted — rotation is an account-owner operation, not an app operation. The server checks that oauth_clients.developer_id matches your session user; otherwise responds 403 forbidden.

Each endpoint is rate-limited per session: rotate is 5 per minute, revoke is 10 per minute.

04 Rotate the secret

POSThttps://api.usequota.ai/developers/apps/:id/rotate-secret

Path parameters

idstringrequiredThe app’s internal UUID (the id field returned by GET /developers/apps). Not the public-facing client_id.

Request

curl -X POST https://api.usequota.ai/developers/apps/$APP_ID/rotate-secret \
  -H "Cookie: quota_session=$SESSION_TOKEN"

Response — 200 OK

{
  "client_secret": "quota_secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "secondary_expires_at": "2026-06-08T17:42:13.000Z"
}
client_secretstringThe new plaintext secret. Returned only here, never readable again.
secondary_expires_atstring (ISO-8601)When the previous secret stops being accepted. Always approximately 30 days in the future. Compute with NOW() + INTERVAL '30 days' server-side, so the exact timestamp is the server’s wall clock at request time.
Email confirmation
After a successful rotation, the account owner receives an oauth_secret_rotated email noting the app name and the secondary expiry.

05 Revoke the previous secret

Use this endpoint when you want to terminate the grace period early — typically because you suspect the previous secret was leaked or compromised. After revocation, only the current primary secret is accepted at POST /oauth/token.

POSThttps://api.usequota.ai/developers/apps/:id/revoke-secondary-secret

Request

curl -X POST https://api.usequota.ai/developers/apps/$APP_ID/revoke-secondary-secret \
  -H "Cookie: quota_session=$SESSION_TOKEN"

Response — 204 No Content

Empty body. The secondary hash and its expiry timestamp are cleared atomically. Calling revoke when no secondary is set is a no-op (still returns 204).

Revocation is immediate and irreversible
Once revoked, the previous secret stops working everywhere on the next request. Any service still using the old secret will receive 401 invalid_client from POST /oauth/token. Confirm every deployment is on the new secret before revoking.

06 Errors

401unauthorizedNo valid developer session cookie. Log in again at POST /auth/login.
403forbiddenThe session user does not own the app identified by :id.
404not_foundNo active app with that UUID. Soft-deleted apps return 404 too.
429rate_limit_exceededRotate is capped at 5 per minute per session; revoke at 10 per minute.