HTTP Authorization Basic Header: How the Base64 Value Is Built, Decoded and Debugged
HTTP Basic authentication sends a username and password on every request in a header that looks like `Authorization: Basic dXNlcjpwYXNz`. The unreadable part is only Base64. It keeps the credentials safe to put in a header, but it does not hide them. This guide shows how the value is built and decoded, and the bugs behind a 401 when the password is definitely correct.
How the Authorization header is built
RFC 7617 defines the scheme. The client joins the user-id and password with a single colon, encodes that string as bytes, Base64-encodes the bytes and sends the result after the word Basic.
user-id : aladdin
password : opensesame
user-pass: aladdin:opensesame
base64 : YWxhZGRpbjpvcGVuc2VzYW1l
GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
The scheme name is case-insensitive, so basic works, but the space after it is required. The token is standard Base64 with +, / and = padding. It is not the URL-safe Base64url variant. Some strict servers reject a token with the padding stripped.
When a request has no valid credentials, the server answers 401 Unauthorized with a challenge that tells the client which scheme to use:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Orders API", charset="UTF-8"
The realm names the protected area, and browsers show it in their login prompt. The optional charset parameter was added by RFC 7617, and UTF-8 is the only value it allows. It tells the client how the server expects non-ASCII characters to be encoded.
Creating the value on the command line and in code
Most HTTP clients build the header for you, which avoids encoding mistakes. Build it by hand only when a tool cannot.
# curl builds the header itself
curl -u 'aladdin:opensesame' https://api.example.com/api/orders
# Building it manually — note printf, not echo
printf '%s' 'aladdin:opensesame' | base64
# YWxhZGRpbjpvcGVuc2VzYW1l
# Decoding a header you captured
printf '%s' 'YWxhZGRpbjpvcGVuc2VzYW1l' | base64 --decode
# aladdin:opensesame
// Node.js
const token = Buffer.from(`${user}:${pass}`, 'utf8').toString('base64');
fetch(url, { headers: { Authorization: `Basic ${token}` } });
// Browser: btoa() only accepts Latin-1, so encode to UTF-8 bytes first
const bytes = new TextEncoder().encode(`${user}:${pass}`);
const token = btoa(String.fromCharCode(...bytes));
import requests
requests.get(url, auth=('aladdin', 'opensesame')) # requests builds the header
import base64
base64.b64encode('aladdin:opensesame'.encode('utf-8')).decode('ascii')
echo 'user:pass' | base64 is the classic mistake. echo adds a newline, so the encoded password ends in \n and the server rejects it. The token looks almost right, but it ends in Cg== or K instead of the expected characters.
Base64 is encoding, not encryption
Anyone who sees the header can decode it in one step: a proxy log, a browser extension, a shared HAR file or a screenshot of the network tab. Base64 only makes arbitrary bytes safe inside an HTTP header.
This has two practical consequences. Basic authentication must only ever travel over HTTPS, because on plain HTTP the password is effectively sent in clear text. And a captured header is a reusable credential. Unlike a session cookie or a short-lived token, it does not expire, so a leaked header keeps working until the password changes.
Treat the header like the password itself. Redact Authorization in application logs, strip it from HAR files before sharing, and never paste a real one into an online decoder that sends input to a server. Browser-only tools decode locally.
Why the server still says 401
When the credentials are right and the request is still rejected, the bytes are usually wrong. Decode your own header and compare it character by character with what you meant to send.
Non-ASCII characters. RFC 7617 does not fix an encoding unless the server sends charset="UTF-8". Older clients and servers often use ISO-8859-1. A password such as pässwort then becomes different bytes on each side, and the check fails with no hint why. Encode as UTF-8 and make sure the server decodes it the same way.
A colon in the username. The server splits on the first colon, so a user-id cannot contain one. RFC 7617 calls such an id invalid. A colon in the password is fine, because everything after the first colon belongs to the password.
"api:key:secret" -> user-id = "api", password = "key:secret" (fine)
user-id "team:ops" -> cannot be expressed in Basic auth (invalid)
Credentials in the URL. https://user:pass@host/ still works in curl, but browsers strip or block it: fetch() throws a TypeError for URLs with embedded credentials. Characters such as @, : and # in a password also need percent-encoding in a URL, which is a second source of errors. Use the header instead.
A proxy in the way. Proxies authenticate with Proxy-Authorization and respond 407, not 401. Some reverse proxies also strip Authorization before it reaches the application. Nginx passes it through by default, but a custom proxy_set_header configuration can drop it.
Browsers, logout and credential caching
When a browser receives a 401 with a Basic challenge, it shows its own login dialog. After a successful login it caches the credentials for that realm and resends them automatically. That is why Basic auth has no real logout: the server cannot tell the browser to forget them.
Common workarounds are to return a 401 from a logout URL, or to request a resource with deliberately wrong credentials. Both are unreliable across browsers. If users need to sign out, use a session or token scheme instead.
API clients avoid this problem entirely. Automated callers should send the header on every request (preemptive authentication) instead of waiting for a challenge, which also saves a round trip.
When Basic auth is still a reasonable choice
Over HTTPS, Basic auth is simple and widely supported. It suits server-to-server calls with an API key used as the password, internal dashboards behind a VPN, staging sites that need to stay out of search engines, and package registries or webhooks where the client cannot run an OAuth flow.
It is a poor fit when users type the password into a browser, when you need expiry, scopes or revocation per client, or when the credential would be embedded in a mobile app. Bearer tokens (OAuth 2.0, RFC 6750) or signed JWTs give you expiry and scoping. Store server-side Basic credentials hashed like any other password.
Sources and further reading
RFC 7617 defines the Basic scheme, the charset parameter and the rule about colons; it obsoletes the Basic part of RFC 2617. RFC 9110 defines 401, 407, WWW-Authenticate and Authorization. RFC 4648 defines the Base64 alphabet. MDN documents the header and the browser behaviour.
Frequently asked questions
How do I decode a Basic auth header?
Take the part after `Basic ` and Base64-decode it: `printf '%s' 'dXNlcjpwYXNz' | base64 --decode` prints `user:pass`. Everything before the first colon is the username and everything after it is the password.
Is Basic authentication secure?
Only over HTTPS. Base64 is trivially reversible, so on plain HTTP the password is effectively sent in clear text. Even over HTTPS, the header is a long-lived credential and must be kept out of logs and shared captures.
What is the format of the Basic Authorization header?
`Authorization: Basic <base64(user-id:password)>`. The user-id and password are joined with a single colon, encoded as bytes, preferably UTF-8, and Base64-encoded with standard padding.
Can the password contain a colon?
Yes. The server splits on the first colon only, so everything after it is the password. The username cannot contain a colon.
Why does my Basic auth header work in curl but not in my code?
Usually the encoded string differs: a trailing newline from `echo`, a Latin-1 versus UTF-8 mismatch for special characters, or `btoa()` failing on non-Latin-1 text. Decode both headers and compare the bytes.
What is a Basic token?
The Base64 string after `Basic ` in the header. It is not a token in the OAuth sense: it contains the actual username and password and never expires on its own.