API Key Security Testing: Scope, Rotation and Revocation

API Key Security Testing: Scope, Rotation and Revocation
API key security testing means checking that every lifecycle change (a scope reduction, a rotation, an expiry, a revocation, a user leaving) actually changes what the key can do, using your written policy as the oracle and bounded, authorised test keys as the probe. Most teams test that a key works. Far fewer test that it stops working when the policy says it should.
This guide is for teams that own an API with opaque, long-lived keys for customers, integrations or service accounts. It is not an introduction to API security, IDOR or OAuth. It covers the stale-rights problem: the gap between what the dashboard says about a key and what the running system still accepts.
Two things to keep in mind throughout. A revocation test establishes the observed boundary and the observed delay. It does not prove absolute safety. And the semantics are product-specific: an opaque API key, an OAuth access token and an intentionally publishable identifier behave differently, so test the model you actually run.
The key the UI says is revoked (a hypothetical)
Imagine a customer admin clicks revoke on an integration key. The dashboard shows the key as inactive. The audit log records the event. A week later, a test run against one read-only export route, with a dedicated test key revoked the same way, still gets a 200 and a page of data. The main API gateway rejects the key. A second route, served by a different service with its own validation cache, does not.
Nothing about that scenario is exotic. It is what happens when revocation is a database write and enforcement is spread across gateways, caches, replicas, background workers and older API versions. Each one has its own idea of when a key stopped being valid.
That is the failure this guide tests for. The question isn't whether authentication works. It's whether state changes propagate everywhere the key can be used.
First, a distinction that shapes every test below:
- Opaque API keys are bearer secrets looked up server-side. Revocation is a server-side state change, so propagation is your problem.
- OAuth access tokens are often short-lived and sometimes self-contained, so revocation and expiry work differently. Test them against their own documented behaviour.
- Publishable identifiers are meant to be exposed. Stripe, for example, documents that only publishable keys are safe to expose outside your back end (Stripe). Test that they cannot do anything a secret key can.
Do not assume one model's behaviour applies to another.
Write the policy first: owner, tenant, scopes, lifetime, revocation delay, rotation overlap
A test needs an oracle, and the oracle is a written policy. Without one, "the key still worked after revocation" is an argument, not a finding.
Write one page per key type. Each should state:
- Owner. Which user, team or service account owns the key, and what happens to the key when that owner is suspended, removed or reassigned.
- Tenant binding. Which tenant or organisation the key is bound to, and that it cannot reach resources outside it.
- Scopes and resources. Which operations the key may perform, split into read and write, and which resources or projects it may touch. GitHub's fine-grained tokens are a useful concrete example: their rights are bounded by the owner's permissions and by the permissions and repositories selected for the token (GitHub Docs).
- Lifetime. Whether keys expire, and after how long.
- Maximum revocation delay. The longest time between a revoke action and the key being refused everywhere. Write down your actual documented figure, even if it is "within 60 seconds". Do not borrow an immediate-revocation claim you cannot demonstrate.
- Rotation overlap. Whether old and new keys are both valid during a rotation, and for how long.
- In-flight work. What happens to requests already accepted and jobs already queued when a key is revoked.
Teams skip that last one. Preventing new requests and cancelling accepted work are different behaviours. Pick one deliberately and write it down.
The OWASP REST Security Cheat Sheet is a good anchor for the access-control half: non-public REST services must perform access control at each API endpoint, and API keys deserve the same handling as other credentials (OWASP). It does not tell you that an API key is a user identity, so your policy has to.
What are the current best practices for API key lifecycle management?
Write down an owner, tenant, scopes, maximum lifetime, revocation delay and rotation overlap for every key class, then test each of those claims rather than trusting the UI. Prefer short lifetimes and scheduled rotation, bind keys to a named owner so offboarding triggers revocation, and log every issue, use, change and revoke event. Treat the policy as testable: each line should map to an automated check in CI that fails when behaviour drifts.
Set up a bounded test: staging, dummy data, dedicated test identities and keys
Run lifecycle tests in staging, or in a production environment you have explicit written authority to test. Either way, bound them.
- Dedicated test identities. Create test users, teams, tenants and service accounts that exist only for this purpose. Do not reuse a real engineer's account.
- Dedicated test keys. Issue keys for those identities with known scopes. Label them so nobody mistakes them for production credentials.
- Dummy data. Seed each test tenant with synthetic records. Never use live customer data.
- Two tenants. Use at least two, so you can check that a key bound to tenant A stays out of tenant B before and after each lifecycle change.
- Low volume. These are single-request checks and short polling loops, not load. Send no mass traffic, scan no unrelated hosts, and use no payloads designed to break things.
- Evidence hygiene. Record key IDs or fingerprints and request correlation IDs. Never store raw secrets or full
Authorizationheaders in test output, CI logs or tickets.
If your platform has separate sandbox and live modes, use them as intended. Stripe documents that all API requests occur in either sandbox or live mode (Stripe). That gives you a natural boundary and a natural test: a sandbox key must not work against live resources.
For guidance on what to agree before touching production at all, see Offensive Security Testing in Production: What to Agree First.
Scope and ownership tests: issue time vs use time, reductions, role changes, membership removal, suspension, service accounts
To test API key scopes with automated tests, assert enforcement at two moments: when the key is issued and every time it is used. The common bug is a scope that is validated at creation and then trusted forever.
Issue time vs use time. Try to issue a key with a scope the requesting user does not hold. It should be refused. Then take a valid key and call an endpoint outside its scope. It should be denied with a consistent error, and the check should hold on every route and every supported API version, not just the main one. Older versions and internal-facing paths are where scope checks go missing.
Read versus write. For each scope, test one read operation and one write operation. A read-only key that can update a record is a finding. So is a key that is denied the write but leaves partial state behind. Check the resource afterwards, not just the status code.
Scope reductions. Reduce a key's scope from write to read, then retry a write. Under your policy, the reduction should apply within the documented delay. Poll at short intervals and record when behaviour changes. If the old scope persists until the key is rotated, that is either a policy statement or a finding. Your written policy decides which.
Role changes. Where a key inherits the owner's rights, demote the owner and test the key. GitHub documents that a fine-grained token's access is bounded by what its owner can do (GitHub Docs), so a demotion should narrow the token. If your keys are meant to be independent of the creator's role, the policy should say so, and the test should confirm that independence rather than assume it.
Membership removal. Remove the owner from the organisation or team. Does the key stop working, keep working, or transfer to a team owner? Each is a legitimate design. Only one is yours, and the policy should name it.
Suspension. Suspend the owning user and test their keys. Suspension is often implemented in the user table and missed in the key validation path.
Service accounts. Check who owns a service-account key when the person who created it leaves. Orphaned keys with no accountable owner are a lifecycle gap even when they are technically valid.
Tenant-bound resources. After every change above, repeat one cross-tenant request with the same key. A lifecycle change should never widen tenant reach. For the broader object-access side, see IDOR Testing for APIs: How to Find Broken Object Access and Tenant Isolation Testing: How to Prove Tenant Boundaries Hold.
How do you automate scope and permission tests for API keys?
Create a dedicated test key per scope set, then script one request per scope and per endpoint family, asserting an allow for in-scope calls and a 403 for everything else. Rerun the same matrix after each lifecycle change: scope reduction, role change, owner removal and suspension. Assert on the response a second after the change and again after your documented revocation delay, so you catch caches and gateways that lag. Keep the request, response and timestamps as evidence, and run the matrix in CI against staging.
How do you test least privilege scope enforcement for API keys?
Start from the minimal scope a key needs, then try to exceed it: call endpoints outside the scope, use other tenants' resource IDs, attempt writes with a read-only key, and try to mint or modify keys with a key that should not manage keys. Enforce the check at use time, not just issue time, so a scope reduction or role downgrade takes effect on existing keys. Treat any call that succeeds beyond the intended scope as a finding, and keep the reproduction steps for the retest.
Rotation, expiry and revocation tests: overlap, cutover, rollback, caches, gateways, replicas and queued work
Good API key rotation testing answers one question: during and after rotation, does each key behave exactly as the policy says? Zero-downtime rotation depends on a defined overlap window, so test the window, not just the endpoints.
Overlap. If the policy allows old and new keys to coexist, issue the new key and confirm both work during the window. Confirm the new key carries the same scopes as the old one, no more. Then wait out the window and confirm the old key is refused. A grace period with no end is a second long-lived key.
Cutover. After the overlap closes, test every route class again with the old key: reads, writes, any streaming or webhook-adjacent endpoints, and older API versions. Any route that still accepts the old key is the finding.
Rollback. Simulate a failed rollout. A deployment fails halfway, and the team wants to go back to the old key. Does your process allow reactivating a rotated key? Some systems do not. Stripe documents that rotating a key revokes it and generates a replacement ready to use immediately, with guidance on rotating safely to avoid downtime (Stripe). Whatever your system does, test the rollback path and write it down, because the alternative is an engineer improvising at 2am.
Expiry versus revocation. Test them separately. Expiry is time-driven and should need no action. Revocation is event-driven and depends on propagation. Stripe's documentation treats them as distinct: expiring a key means you must create a new one and update any code using the expired key (Stripe). Check that an expired key fails on the first request after the expiry timestamp, within your documented clock tolerance.
Caches, gateways and replicas. This is where revocation actually breaks. Write an integration test for revocation propagation that does this:
- Make a successful request with a test key and record the correlation ID.
- Revoke the key through the normal admin path.
- Poll a representative request at short intervals against each enforcement point: the edge gateway, each service that validates keys independently, and any read-replica-backed lookup.
- Record the time of the first denial at each point and the time of the last success.
- Compare the longest observed delay to the maximum in your policy.
The result is the observed boundary and delay. If the policy says 60 seconds and one service takes 10 minutes because of a cache time-to-live, you have a concrete, reproducible finding.
Chaos-style variants. To chaos test secret rotation, inject the faults that happen in real rollouts: delay propagation to one node, restart a service mid-rotation, run a consumer that still holds the old key, and fail the rotation job halfway. Keep it small and staged. The goal is to see whether a partial failure leaves a key valid that should not be, or invalid that should be live.
Queued and in-flight work. Revocation stops new requests. It does not automatically cancel work already accepted. Test it: start a long-running job or queue a task with a test key, revoke the key, and observe whether the job completes, is cancelled or is re-authorised before each step. None of these is automatically correct. It is a product-policy choice. A job that moves money or exports data after the key was revoked may be unacceptable. A job that is half-written when cancelled may be worse. Decide, document, test.
Check state, not just status codes. A 403 does not prove nothing happened. After a denied write, read the resource and confirm no forbidden state change occurred. After a revoked-key request to an async endpoint, confirm no job was enqueued.
What are the best practices for testing API key rotation?
Test the overlap window as a boundary, not a happy path. Issue the new key, confirm both keys work inside the documented overlap, then confirm the old key fails the moment the overlap ends, including through gateways, caches and read replicas. Also test rollback (can the old key be reinstated, and should it be), rotation while requests are in flight, and queued or scheduled jobs that captured the old key. Check that the old key's scopes cannot be inherited by the new one unintentionally.
What should an API key revocation testing checklist include?
Check that a revoked key is rejected on every path: the main API, gateway, CDN or edge cache, read replicas, webhooks, background workers and any queued job already holding the key. Measure the actual delay between the revoke action and the first rejected request, and compare it to your policy. Confirm the UI, audit log and API all agree on the key's state, that long-lived connections such as streams are dropped, and that the revoked key stays rejected after a deploy or cache flush.
Credential lifecycle transitions: expected behaviour, edge cases and evidence to keep
Use this as a test-case matrix. The "expected" column is a typical default. Your written policy overrides it.
| Transition | Expected: new requests | Policy-dependent edge cases | Evidence to retain |
|---|---|---|---|
| Key issued | Allowed only for granted scopes and bound tenant | Issuing a scope the requester lacks; creating keys for other users | Key ID, requester ID, granted scopes, correlation ID |
| Scope reduced (write to read) | Writes denied within documented delay; reads allowed | Does reduction apply to existing keys or only new ones; cached scope lag | Key ID, timestamps of last write success and first denial |
| Owner role demoted | Key rights narrow if tied to owner; unchanged if independent | Inherited vs independent rights; team-owned keys | Owner ID, role before and after, key behaviour before and after |
| Owner removed from team or org | Key denied, transferred or kept, per policy | Service-account keys; keys with no remaining owner | Membership event ID, key state, first-denial timestamp |
| Owner suspended | Owner's keys denied | Keys shared across a team; keys on other API versions | Suspension event, per-route results |
| Rotation, during overlap | Both keys work; new key has equal scope | Overlap with no end; new key broader than old | Both key IDs, window start and end |
| Rotation, after cutover | Old key denied everywhere | Older API versions; webhook-adjacent routes; replicas | Old key ID, route list, denial timestamps |
| Rotation, failed rollout | Defined rollback path; old key reactivation only if policy allows | Half-rotated fleets; consumers on old key | Rollout log, key states, rollback outcome |
| Key expired | Denied on first request after expiry, within clock tolerance | Clock skew; cached validity | Expiry timestamp, first-denial timestamp |
| Key revoked | New requests denied within maximum documented delay | Per-gateway and per-cache lag; replica lag | Per-enforcement-point first-denial times, observed maximum |
| Revoked with in-flight work | Per policy: complete, cancel or re-authorise | Money movement, exports, partial writes | Job ID, state before and after, resource state |
| Sandbox key used against live | Denied | Misconfigured environment routing | Key ID, environment, response |
Keep a record for each test: key ID or fingerprint, correlation IDs, UTC timestamps, expected result from the policy, observed result, and the resource state afterwards. Never keep the raw secret or a full Authorization header.
CI regression plan, reviewer guidance and the finding-to-fix-to-retest loop
CI regression plan. Turn the policy into assertions. Each line in the written policy becomes one automated test against staging, using dedicated test keys:
- A reduced-scope key is denied the removed operation within the policy's maximum delay.
- A rotated key is refused after the overlap window, on every route class and API version in your inventory.
- A revoked key is refused at every enforcement point within the documented delay.
- A removed or suspended owner's key behaves as the policy states.
- A denied write leaves no forbidden state change.
- A key bound to tenant A cannot read tenant B after any of the above.
Ordinary test code can replay these on every deploy that touches auth. Run the propagation test on a schedule as well as on merge, because caches and infrastructure drift without a code change. Fail the build on a violation of the written delay, not on a hard-coded number that nobody remembers.
Reviewer guidance. Treat changes to credential validation, key caches and invalidation logic as security-sensitive. When reviewing, ask:
- Does this change add a new place where keys are validated, and is revocation wired to it?
- Does a new cache have a time-to-live shorter than the policy's maximum revocation delay, or an explicit invalidation hook?
- Does a new route, version or background worker call the same validation path as the rest?
- Is scope checked at use time, not only copied from the key at creation?
- What happens to queued work when the key is revoked?
For catching these before merge, see PR Security Review Tools Compared: Diff Checks Before Merge and Security Regression Testing for Authorisation Fixes.
Finding, fix, retest. When a test fails, write the finding against the policy line it violates: the test key ID, the route, the expected and observed behaviour, the measured delay, and the correlation IDs. Fix the enforcement gap, not the symptom. If one service ignored revocation, ask which others share its validation path. Then add the failing case to the CI suite so it cannot quietly return, and retest the original reproduction after the fix ships. A finding is closed when the same bounded test now passes, not when a ticket moves to done. How Automatic Retests Verify a Fix When Your PR Merges covers that loop in more detail.
Test the policy line you can't currently prove
Pick the one line in your key policy you cannot currently demonstrate, probably the maximum revocation delay or what happens to in-flight work, and write a bounded staging test for it this week. Everything else in this guide follows from having that oracle and a dedicated test key. If you want help finding stale rights in authorisation, API and tenant boundaries, and having fixes retested, Borg's pentests run against the full application with verified findings and reproduction steps. Book a walkthrough at borghq.io.
Frequently asked questions
- What can a stolen secret API key allow?
- It depends on the documented key model. An unrestricted secret key may allow everything the account can do, while a restricted key is limited to its granted permissions. Stripe, for example, recommends restricted keys for most uses ([Stripe](https://docs.stripe.com/keys)). Test the worst case: what can your broadest key read, change and move, and how quickly can you revoke it?
- Is key rotation the same as revocation?
- No. Rotation replaces a key, usually with an overlap so consumers can switch without downtime. Revocation ends a key's validity, ideally everywhere and quickly. Some systems revoke the old key as part of rotating it, as Stripe documents, but others leave it valid until overlap ends. Test both separately against your written policy.
- How do I test API key revocation propagation?
- Revoke a dedicated test key through the normal admin path, then poll a representative request at short intervals against every enforcement point: gateway, each service that validates keys, and replica-backed lookups. Record the first denial at each. The longest observed delay is your real boundary, and it should sit inside your documented maximum.
- Does removing a user revoke all their API keys?
- Only if your design says so. Some systems tie keys to the owner and deny them on removal, others keep team-owned or service-account keys alive, and others transfer ownership. Check the documented behaviour, then test removal and suspension with a dedicated owner and keys, including keys on older API versions.
- How do API keys differ from OAuth access tokens for testing?
- API keys are typically long-lived opaque secrets, so revocation is a server-side state change you must propagate and test. OAuth access tokens are often short-lived and sometimes self-contained, so expiry and refresh behave differently. Test each against its own documented semantics rather than assuming the same rules apply.



