The Authorization Header and Friends
Where credentials actually travel, why the query string is the wrong place, and how a server advertises what it accepts.
Authorization: Bearer <token>HTTP reserves one header for credentials: Authorization. Its value is a scheme followed by the credential.
Authorization: <scheme> <credential>The scheme name tells the server how to read what follows.
| Scheme | Looks like | Used by |
|---|---|---|
Basic | Basic dXNlcjpwYXNz | base64 of user:password |
Bearer | Bearer eyJhbGci... | JWTs, OAuth access tokens |
Digest | Digest username=..., response=... | legacy challenge/response |
AWS4-HMAC-SHA256 | Credential=..., Signature=... | request signing |
Why not the query string?
Because URLs are written down everywhere: server access logs, CDN logs, browser history, the Referer header sent to third parties, bug-report screenshots, and analytics. A token in ?api_key=... ends up in all of them. A token in a header ends up in none of them by default.
The challenge: WWW-Authenticate
When a server rejects a request for lack of credentials it should say what it wanted:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token",
error_description="The access token expired"This is what makes a generic HTTP client able to react correctly — and it is what makes a browser pop up the native username/password box for Basic.
Cookies are the other channel
Cookies are not sent in Authorization; they get their own Cookie header, and crucially the browser attaches them automatically. That single difference is the source of nearly every trade-off in this course: automatic attachment is convenient, and it is exactly what makes CSRF possible.
Custom headers
X-API-Key: ... is common and perfectly workable. Be aware it is a non-simple header for CORS, so a cross-origin browser request carrying it triggers a preflight — the same as Authorization does.
Example
# Bearer token — the modern default
curl https://dfg.com/api/me -H "Authorization: Bearer $TOKEN"
# Basic — curl builds the base64 for you
curl https://dfg.com/api/me -u alice:s3cret
# sends: Authorization: Basic YWxpY2U6czNjcmV0
# Custom API key header
curl https://dfg.com/api/me -H "X-API-Key: $API_KEY"
# Cookie — normally the browser does this for you
curl https://dfg.com/api/me -H "Cookie: sid=8f2b..."
# ❌ Do not do this: it lands in every access log on the path
curl "https://dfg.com/api/me?api_key=$API_KEY"When to use it
- An SDK reads the WWW-Authenticate header on a 401 to decide between refreshing the token and prompting for a fresh login.
- A security review finds API keys in a year of nginx access logs because an old client passed them as query parameters.
- A logging middleware redacts Authorization and Cookie headers so that support engineers can read production traces without seeing live credentials.
More examples
Parsing the header safely
Splitting on every colon is a classic bug: it truncates any password containing one. Split on the first colon only.
function readBearer(req) {
const header = req.get('authorization');
if (!header) return null;
// Scheme is case-insensitive per RFC 7235; the space is a single SP.
const [scheme, ...rest] = header.split(' ');
if (scheme.toLowerCase() !== 'bearer') return null;
const token = rest.join(' ').trim();
return token.length ? token : null;
}
// Basic is base64 of "user:password" — decoding is not decrypting.
function readBasic(req) {
const header = req.get('authorization') || '';
if (!/^basic /i.test(header)) return null;
const decoded = Buffer.from(header.slice(6), 'base64').toString('utf8');
const i = decoded.indexOf(':'); // password may contain ':'
return i < 0 ? null : { user: decoded.slice(0, i), pass: decoded.slice(i + 1) };
}Redacting credentials in logs
Redact by allowlist-of-shape rather than by hand at each call site — one forgotten logger is enough to spill tokens into a log aggregator you do not control.
const SENSITIVE = new Set(['authorization', 'cookie', 'set-cookie', 'x-api-key']);
function safeHeaders(headers) {
return Object.fromEntries(
Object.entries(headers).map(([k, v]) =>
[k, SENSITIVE.has(k.toLowerCase()) ? '[redacted]' : v])
);
}
app.use((req, res, next) => {
logger.info({ method: req.method, url: req.originalUrl,
headers: safeHeaders(req.headers) });
next();
});
Discussion