HTTP Basic authentication places an encoded username and password in an Authorization header. The encoding is reversible, so possession of that header can be equivalent to possession of the credential. Basic defines a challenge-response exchange, but it does not cryptographically bind a response to a fresh server nonce. A captured accepted value can be replayed while the server continues to accept that account and credential.
Understand the actual exchange
A server can reply with 401 and a WWW-Authenticate: Basic challenge naming a realm. The client may then retry with an Authorization header. A client can also send credentials preemptively within an appropriate protection space. This means authenticated requests carry the credential; it does not mean every initial request must already contain it. The realm helps identify the protection space, rather than granting permissions to every path with a similar label.
The encoded input joins the user identifier, a colon, and the password. The first colon is the separator, so a username cannot contain one while a password can. Base64 is neither encryption nor password hashing. For non-ASCII credentials, character encoding matters: RFC 7617 leaves the default encoding unspecified within ASCII-compatible constraints and allows the challenge to indicate UTF-8. An ASCII demonstration does not validate every internationalized credential path.
Basic itself does not specify a session token or a logout protocol. An application may impose account expiry, disable an account, change authorization, or reject an old password. Therefore password rotation is not the only way to stop acceptance of a captured header. Define revocation and credential lifetime in the server’s actual account policy rather than assuming the wire scheme supplies them.
When it breaks
On an unprotected network path, an observer who reads the header can decode and reuse it. HTTPS with proper certificate validation protects transit to the TLS endpoint. It does not hide the credential from that endpoint, an authorized terminating proxy, or a careless application log. Protect subsequent hops and exclude Authorization values from routine traces, reports, and shared diagnostic captures.
Credentials with broad privileges magnify a leak. Use accounts appropriate to the task and its lifetime, and apply authorization after authentication. A valid credential does not imply permission to perform every operation. A realm label is not a tenant isolation mechanism. Restrict where a client sends credentials, particularly across redirects or when a script reuses a global header for unrelated destinations.
Repeated 401 responses can reflect missing, incorrect, expired, or unsupported credentials. In this example, 403 means the recognized demo account lacks the requested permission, but the general status does not prove that authentication succeeded in every implementation. Check the service’s contract and response challenge rather than inferring the entire identity state from one code.
Check it quickly
Save this as basic-auth.py and run python3 basic-auth.py. All credentials are deliberately synthetic: the username is DEMO and the password is NOT:A:SECRET. The server binds only to loopback and uses plain HTTP to make the protocol mechanics inspectable. Do not replace the demonstration values with real credentials. No TLS, persistent account store, or production authentication service is tested.
from base64 import b64decode, b64encode
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
import hmac
import json
synthetic = b'DEMO:NOT:A:SECRET'
authorization = 'Basic ' + b64encode(synthetic).decode('ascii')
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
accepted = hmac.compare_digest(self.headers.get('Authorization', '').encode('latin1'),
authorization.encode('ascii'))
status = 401 if not accepted else 403 if self.path == '/admin' else 200
data = {401: b'credentials required\n', 403: b'permission denied\n', 200: b'allowed\n'}[status]
self.send_response(status)
if status == 401:
self.send_header('WWW-Authenticate', 'Basic realm="local-demo", charset="UTF-8"')
self.send_header('Cache-Control', 'no-store')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = Thread(target=server.serve_forever)
thread.start()
try:
observed = []
for path, credential in [('/', None), ('/', authorization), ('/', authorization),
('/admin', authorization), ('/', 'Basic invalid')]:
connection = HTTPConnection('127.0.0.1', server.server_port, timeout=5)
try:
connection.request('GET', path, headers={'Authorization': credential} if credential else {})
response = connection.getresponse()
observed.append({'status': response.status,
'challenge': response.getheader('WWW-Authenticate'),
'body': response.read().decode()})
finally:
connection.close()
assert [r['status'] for r in observed] == [401, 200, 200, 403, 401]
assert observed[0]['challenge'] == 'Basic realm="local-demo", charset="UTF-8"'
user, password = b64decode(authorization.split()[1], validate=True).split(b':', 1)
assert user == b'DEMO' and password == b'NOT:A:SECRET'
print(json.dumps({'responses': observed, 'synthetic_decoded_user': user.decode(),
'synthetic_decoded_password': password.decode(), 'replay_accepted': True}))
finally:
server.shutdown()
thread.join()
server.server_close()
The first request receives 401 with the Basic challenge. Two subsequent requests with the same encoded value both receive 200, demonstrating replay while that credential remains accepted. The demo account receives 403 for /admin, and an invalid header receives another 401. No server log records request headers.
The final assertions decode the synthetic value and split it at the first colon. The remaining colons belong to the password. This verifies reversible encoding, challenge delivery, repeated acceptance, and a separate permission decision. The handler recognizes one fixed demonstration value; it is not a complete authentication implementation. A production review must still examine credential storage, throttling, revocation, transport, and authorization together.
Sources
- RFC 7617: Basic authentication, challenge, encoding, credential reuse, and protection spaces, checked 2026-09-13.
- MDN: HTTP authentication, authentication headers and transport considerations, checked 2026-09-13.