HTTP Basic Authentication

The oldest scheme in HTTP: send the username and password on every request, base64-encoded.

SyntaxAuthorization: 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

  1. The client requests a protected resource with no credentials.
  2. The server replies 401 with WWW-Authenticate: Basic realm="api".
  3. A browser shows its native login dialog; a script just builds the header itself.
  4. The client retries with the Authorization header.
  5. The server decodes it, verifies the password against a hash, and either serves the resource or returns 401 again.

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.
  • curl scripts 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

Example · bash
# 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:s3cret

When 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.

Example · javascript
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.

Example · php
<?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

  • Be the first to comment on this lesson.