Quick answer: Most teams mock the 200 and then trust their client's auth handling, yet the auth failures are the responses that actually protect an API and exercise the client's most complex logic: expired tokens, insufficient scope, malformed credentials, and the distinction between "retry after refresh" and "reject and re-authenticate." A mock that only returns success leaves the security boundary entirely untested, so the first 401 or 403 a client sees is in production, where the error handling may loop, expose the token, or log the user out incorrectly. The reliable approach is to model auth failure as a first-class set of scenarios, return the exact status codes and challenge headers the real gateway emits, and assert the client's specific response to each rather than treating every denial as the same generic error. If your mock cannot produce an expired-but-refreshable token and a hard scope rejection separately, you are not testing auth at all.
Authentication in a mock is easy to overlook because the happy path dominates early development, and a token is often wired in as a static header so every request succeeds. The real gateway, however, behaves as a stateful decision engine: it inspects the token's expiry, issuer, audience, and scope, and returns different codes depending on which condition failed. These distinctions matter because the client's correct reaction is different for each one, and a mock that collapses them into a single generic error cannot verify that logic.
Auth failure scenarios and the correct client reaction
The status code and the token condition should drive distinct, testable client behavior.
| Condition | Gateway response | Correct client behavior |
|---|---|---|
| Expired but valid token | 401 + refresh path | Refresh and retry once |
| Missing/malformed token | 401 + WWW-Authenticate | Re-authenticate, no retry |
| Valid token, wrong scope | 403 | Reject, do not retry |
| Bad issuer/audience | 401 | Treat as invalid, re-auth |
| Revoked token | 401 | Force re-login |
The scope row is the most important distinction to preserve: a 403 from a valid token with insufficient scope must never trigger a refresh, because the new token will carry the same scope and fail again, creating a pointless loop. The client should surface an authorization error instead.
Return exact status codes and challenge headers
Configure the mock to inspect the presented token the way a gateway does and return the matching code with the correct headers, including the challenge that tells the client what authentication is accepted.
Because the mock returns the same structure and headers as the real gateway, the client's parsing and branching run against realistic responses rather than a fabricated error object, and the test can assert that each code reaches the correct branch.
Assert the specific behavior for each failure
For each scenario, drive the client and assert the resulting action rather than just the received status, so the handling logic itself is verified.
Separating these assertions is what catches the two most common real-world bugs: a client that retries forever on a hard 403, and one that forces a full logout on a quietly expired token that a single refresh would have fixed.
Generate the auth mock from the contract
When the security schemes, required scopes per operation, and error responses are declared in the OpenAPI contract, a generated mock can enforce scope per endpoint and return the correct challenge and error shapes automatically. The same contract drives generated clients that already know how to refresh and branch, so the mock and the client agree on the auth model rather than each encoding a separate guess about what the gateway does.
The practical rule is to treat auth failures as a dedicated test matrix rather than an afterthought. Reproduce expired, malformed, wrong-scope, and revoked tokens with the exact status codes and headers, and assert the distinct client reaction for each, including the no-refresh rule for 403. If every mocked request simply succeeds, the security boundary and the most intricate branch of your client remain untested until a real token fails.
Frequently asked questions
Why mock auth failures if the happy path works?
The happy path never exercises the client's refresh, re-authentication, and error-surfacing logic. Those branches only run on a 401 or 403, and testing them in production is too late.
What is the difference between a 401 and a 403 here?
A 401 means authentication failed or the token expired, which may be refreshable; a 403 means an otherwise valid token lacks the required scope, and refreshing will not help, so the client must not retry.
Should a 403 ever trigger a token refresh?
No. A new token carries the same scope and fails again, creating a loop. The client should surface an insufficient-scope error instead.
How many times should the client retry an expired token?
Once, after a refresh. If the refreshed call still fails, the client routes to re-authentication rather than looping.
How does the contract keep the mock accurate?
It declares the security schemes, per-operation scopes, and error shapes, so a generated mock enforces scope and returns realistic challenges while generated clients share the same auth model.
This is part of the Powerduck vs WireMock comparison — start there for the full picture of reliable auth mocking.
Comments
Join the discussionNo comments yet. Be the first to comment.

