Almost every API you'll test in real work is gated by authentication of some kind. The simplest two — API keys and basic authentication — are still everywhere despite being the oldest schemes around. They show up in internal tools, third-party services, and "quick prototype" endpoints that ended up in production. Understanding how they work, how to test them properly, and where they fall down is foundational to everything that follows in this chapter.
API keys
An API key is a long random string that identifies the calling application. Pass it with the request and the server knows who you are.
Two ways APIs accept keys:
- In a header (preferred):
Some APIs use the standard
X-API-Key: abc123def456ghi789Authorizationheader instead:Authorization: ApiKey abc123def456ghi789 - As a query parameter (less safe):
https://api.example.com/data?api_key=abc123def456ghi789
Why the query-string variant is worse: URLs end up in proxy logs, browser history, server access logs, and analytics tools. A leaked URL becomes a leaked credential. Headers don't share that exposure surface.
API keys identify the calling app, not a specific user. They're great for service-to-service communication, public APIs (Stripe, OpenAI, Twilio all use them), and rate-limiting per consumer. They're not great for user-level permissions — for that you want OAuth or JWTs (next two lessons).
Basic authentication
Basic auth is even older than API keys. It sends the username and password on every request, encoded (not encrypted!) as Base64:
Authorization: Basic YWxpY2U6cGFzc3dvcmQ=
That string is just base64("alice:password"). Decode it and you have the password.
curl has built-in support:
curl -u alice:password https://api.example.com/usersThree things to remember about basic auth:
- Never use it without HTTPS. Base64 is reversible; over plain HTTP, the credential is effectively in the clear.
- It sends credentials with every request. That's more exposure than a token-based scheme.
- It's still common in internal tools, CI integrations, and legacy enterprise APIs. You'll meet it.
How API key auth flows
Step 1 of 5
Client adds key
The client sets an Authorization or X-API-Key header on the outgoing request, pulling the key from environment variables or a secret manager.
Five steps, the same shape every time. Your tests must cover each step's failure mode.
A test matrix for API key auth
For any endpoint protected by an API key, you should have at least these tests:
| Scenario | Setup | Expected |
|---|---|---|
| Valid key | X-API-Key: <known-good> | 200 with expected body |
| No key | header omitted | 401 Unauthorized |
| Empty key | X-API-Key: | 401 |
| Invalid key | X-API-Key: garbage123 | 401 |
| Revoked key | key disabled in admin panel | 401 |
| Wrong header name | key in X-Api-Key instead of X-API-Key (if case-sensitive) | 401 |
| Insufficient scope | read-only key on a write endpoint | 403 Forbidden |
| Key in query when only header allowed | ?api_key=... instead of header | 401 |
Eight scenarios. None overlap; each catches a different bug.
A subtle one worth flagging: 401 vs 403. A missing or malformed key is a 401 (we don't know who you are). A valid key that lacks permission is a 403 (we know who you are, but you can't do this). Your tests should expect the right one.
Testing basic auth
The same matrix shape, adjusted for credentials:
| Scenario | Setup | Expected |
|---|---|---|
| Valid credentials | -u alice:password | 200 |
| Wrong password | -u alice:wrongpass | 401 |
| Non-existent user | -u nobody:anything | 401 |
| Empty username | -u :password | 401 |
| Empty password | -u alice: | 401 |
No Authorization header | header omitted | 401 |
Notice that "non-existent user" and "wrong password" should return the same 401. Returning a different status (or a different error message) tells an attacker which usernames exist — a textbook user-enumeration vulnerability.
Storing keys safely in test code
The cardinal sin of API testing: hardcoding credentials in test files and committing them to git.
# DON'T
api_key = "sk_live_abc123def456ghi789"
# DO
api_key = os.environ["API_KEY"]A few practical patterns:
- Keep secrets in environment variables, locally and in CI (
.envfiles for dev, GitHub Actions secrets for CI). - Never log the key value. Log only "key present: yes/no" or the first few characters.
- Rotate test keys regularly, and have the ability to revoke a leaked key fast.
- If a key is committed by accident, revoke it immediately even if you removed it in a follow-up commit. Git history is forever.
⚠️ Common mistakes
- Confusing 401 and 403. Permission tests should expect 403; missing-credential tests should expect 401. Conflating the two hides bugs in either direction.
- Putting keys in URLs because it's "easier." Query-param keys end up in logs, analytics, and screen-shared URL bars. Use headers.
- Treating Base64 as encryption. It's not. Anyone who sees a basic-auth header can decode it instantly. HTTPS is the only thing keeping it private on the wire.
🎯 Practice task
Test API key auth on a real public API. 20 minutes.
- Sign up for a free OpenWeatherMap or The Cat API account and get an API key. (Both have generous free tiers and require keys.)
- Make an authenticated GET with curl using the key in the documented header. Confirm 200.
- Repeat the request with no key. Note the status code and error message.
- Repeat with a deliberately bad key (
X-API-Key: not-a-real-key). Compare to the no-key response. - Try the same key as a query parameter (most APIs accept either form). Note where the key now appears in
curl -voutput (URL line vs header line). - Browse the API Testing Concepts cheat sheet for the section on auth — confirm your test matrix covers each scenario listed.
- Stretch: write a short script (Python with
requests, or any language you know) that reads the key fromos.environ, makes the call, and asserts on the status. Run it once with the env var set and once without — confirm the env-var version doesn't leak the key into source.
You've covered the simplest auth schemes. The next lesson tackles the modern standard for delegated authorisation: OAuth 2.0.