HTTP Basic Authentication
The oldest scheme in HTTP: send the username and password on every request, base64-encoded.
Authorization: Basic <base64(user:password)>Basic is defined in RFC 7617 and is as simple as authentication gets. The client joins the username and password with a colon, base64-encodes the result, and sends it in the Authorization header.
Authorization: Basic base64(username + ":" + password)Step by step
- The client requests a protected resource with no credentials.
- The server replies
401withWWW-Authenticate: Basic realm="api". - A browser shows its native login dialog; a script just builds the header itself.
- The client retries with the
Authorizationheader. - The server decodes it, verifies the password against a hash, and either serves the resource or returns
401again.
Base64 is not encryption
This is the single most important thing about Basic. YWxpY2U6czNjcmV0 decodes to alice:s3cret with one command, no key required. Over plain HTTP the password is effectively written in the clear — and it is re-sent on every single request, so an attacker only needs to capture any one of them.
Where it is still fine
- Internal tools behind a VPN or private network.
curlscripts and CI jobs against your own services.- Machine clients where the "password" is really a rotated API token.
Where it is not
- Anything on the public internet holding real user accounts.
- Browsers: there is no clean way to log out (the browser caches the credentials), no MFA, no password reset flow, and the native dialog cannot be styled.
Doing it safely if you do use it
Compare against a proper password hash (bcrypt/argon2), compare fixed-length digests in constant time so response timing does not leak the username, and rate-limit failures hard — Basic endpoints are a favourite target for credential stuffing.
Example
# curl builds the header for you
curl -u alice:s3cret https://dfg.com/api/me
# ...which is exactly this
printf 'alice:s3cret' | base64
# YWxpY2U6czNjcmV0
curl https://dfg.com/api/me -H "Authorization: Basic YWxpY2U6czNjcmV0"
# And it decodes back with no key at all — this is not encryption
echo 'YWxpY2U6czNjcmV0' | base64 -d
# alice:s3cretWhen to use it
- A nightly CI job pulls metrics from an internal service using Basic auth over TLS on a private network, where a full OAuth flow would be pure overhead.
- A legacy printer or IoT device can only speak Basic, so it is given a dedicated machine account with a rotated token as its password and no other permissions.
- A team protects a staging environment with Basic auth at the reverse proxy to keep crawlers and casual visitors out, entirely separately from the app's own login.
More examples
A correct Basic verifier
The dummy-hash comparison for unknown users is deliberate: without it, a fast 401 for 'no such user' versus a slow 401 for 'wrong password' hands an attacker a valid-username oracle.
import bcrypt from 'bcrypt';
import { timingSafeEqual, createHash } from 'crypto';
const eq = (a, b) => { // constant-time compare of equal-length digests
const ha = createHash('sha256').update(a).digest();
const hb = createHash('sha256').update(b).digest();
return timingSafeEqual(ha, hb);
};
export async function basicAuth(req, res, next) {
const header = req.get('authorization') || '';
if (!/^basic /i.test(header)) return challenge(res);
const decoded = Buffer.from(header.slice(6), 'base64').toString('utf8');
const i = decoded.indexOf(':');
if (i < 0) return challenge(res);
const user = decoded.slice(0, i);
const pass = decoded.slice(i + 1);
const account = await db.users.findByUsername(user);
// Always run a hash comparison, even for an unknown user, so the response
// time does not reveal which usernames exist.
const hash = account?.passwordHash ?? '$2b$12$invalidinvalidinvalidinvalidinvalidinvalidinvalidinva';
const ok = await bcrypt.compare(pass, hash);
if (!account || !ok) return challenge(res);
req.user = { id: account.id };
next();
}
function challenge(res) {
res.set('WWW-Authenticate', 'Basic realm="api", charset="UTF-8"');
return res.status(401).json({ error: 'authentication_required' });
}The same thing in PHP
Some FastCGI setups do not populate PHP_AUTH_USER; you then have to read HTTP_AUTHORIZATION and decode it yourself, which is a common deployment surprise.
<?php
// PHP fills these in from the Authorization header when it is present.
$user = $_SERVER['PHP_AUTH_USER'] ?? null;
$pass = $_SERVER['PHP_AUTH_PW'] ?? null;
if ($user === null) {
header('WWW-Authenticate: Basic realm="api"');
http_response_code(401);
echo json_encode(['error' => 'authentication_required']);
exit;
}
$account = findUserByUsername($user);
$hash = $account['password_hash'] ?? '$2y$12$invalidinvalidinvalidinvalidinvalidinvalidinvalidinva';
if (!$account || !password_verify($pass, $hash)) {
header('WWW-Authenticate: Basic realm="api"');
http_response_code(401);
echo json_encode(['error' => 'invalid_credentials']);
exit;
}
echo json_encode(['id' => $account['id'], 'username' => $user]);
Discussion