December 17, 2003
I wish I didn't need to write this article. My life would be much simpler if Atom could just use existing HTTP authentication, as-is. But it can't; I'm going to tell you why and then I'm going to tell you what we're doing instead.
Let's back up. Atom, in case you missed it, is a new standard that uses XML over HTTP to publish and syndicate web-based content. It is initially targeted at weblogs, and most of the early adopters so far have been weblog vendors and users. It consists of the Atom API, which I discussed last month, and the Atom syndication format, which I will discuss next month. This month I want to talk about authentication.
As with all design decisions, it helps to list the problems you are trying to solve,
the audience you are trying to solve them for, in the form of a character sketch.
about Bob. Bob has a weblog. Bob hosts his weblog on a low-end web hosting service,
which hosts several hundred weblogs on a single machine (a single IP address). Bob
files to his web directory, and he can run CGI scripts in Perl and Python. But Bob
remote shell access on his server (that costs extra), no
.htaccess rights to
change his Apache configuration (his hosting provider set
no PHP scripts, and no database. He can serve static HTML files and run CGI scripts,
This is not a contrived scenario. Many web hosting providers offer exactly this environment, and two popular weblog publishing systems, Movable Type and Blosxom, run as CGI scripts in this environment. There are thousands of real people like Bob.
Bob also travels a lot, going to various O'Reilly conferences. He would like to be able to post to his Atom-enabled weblog from an Atom-enabled client, without anyone else at the conference (who might be listening in on the wireless network) being able to steal Bob's password or take over his weblog.
As we saw last month, all previous weblog publishing APIs send passwords over the wire in clear text. Clearly none of these APIs will work for Bob. A number of solutions were proposed during the development of Atom, but none of them help Bob.
- Use HTTP basic authentication. This does not technically send passwords over the wire in clear text, but it encodes them in a way that is easily reversible. So this doesn't actually help Bob since it's not an improvement over clear text.
- MD5-hash the password and only send the hash. This would solve the password sniffing problem, since you couldn't reverse engineer the hash to recover the original password. But it doesn't help Bob because it's susceptible to replay attacks. Someone at Bob's conference could sniff Bob's transaction and recover the password hash, then reuse it to post to Bob's weblog themselves. Even though the attacker never learns Bob's actual password, they can still learn enough to successfully pretend to be Bob and take over his weblog.
- Use HTTP basic authentication over SSL. This would solve the password sniffing problem, but it doesn't help Bob because he can't use SSL. Due to the way SSL handshaking works, each SSL certificate requires its own IP address, which Bob doesn't have (remember, his weblog is one among hundreds which all share a single IP address). Also, configuring the web server to listen on port 443 requires root access, which Bob doesn't have.
- Use HTTP digest authentication. This would also solve the password
sniffing problem, and it would solve the replay problem, but most web hosting providers
don't turn on digest authentication (it requires an Apache module that is not on by
default). Even if Bob's ISP had mod_digest_auth enabled, it wouldn't help Bob, because
.htaccessrights to configure his passwords; and, because of the way Apache works, CGIs can't implement digest authentication on their own. (Scripts handled by an Apache module, such as mod_php or mod_perl, can implement HTTP digest authentication. But external CGI processes can't because Apache does not pass the necessary headers along to the CGI script. But that still doesn't help Bob because his hosting provider doesn't offer PHP; and, even if they did, his weblog software doesn't run on PHP anyway.)
It looks like Bob is screwed.
WSSE Username Token
A little-known fact about RFC 2617 is that HTTP authentication is extensible. The RFC defines and Apache has modules for Basic and Digest authentication, but developers are free to define different algorithms for use within the HTTP authentication framework, and servers are free to insist that clients support those algorithms if they want access to the server's resources.
After much haggling, the algorithm we chose was WSSE Username Token (PDF). WSSE is a family of open security specifications for web services, specifically SOAP web services. However, the Username Token algorithm is not SOAP-specific; it can be easily adapted to work within the HTTP authentication framework, and it solves all of Bob's problems.
The algorithm itself works like this:
- Start with 2 pieces of information: username and password.
- Create a nonce, which is a cryptographically random string. This is harder than it sounds; if an attacker can guess your next nonce, they can still attempt a replay attack. Most cryptography libraries have routines to generate decent nonces, the specifics of doing which are beyond the scope of this article.
- Create a "creation timestamp" of the current time, in W3DTF format.
Create a password digest:
An example will help make this clearer.
- Let's say Bob's username is
"bob", and his password is
- Bob creates a nonce,
- Bob created this nonce at
"2003-12-15T14:43:07Z", so that's the creation timestamp.
- Bob's password digest is
Base64(SHA1 ("d36e316282959a9ed4c89851497a717f" + "2003-12-15T14:43:07Z" + "taadtaadpstcsm")), which is
"quR/EWLAV4xLf9Zqyw4pDmfV90Y=". Most languages have built-in libraries to create SHA-1 hashes and to encode strings in Base64 format.
Now let's see how this algorithm fits into the HTTP authentication framework.
Extending HTTP authentication
Bob's weblog is at
http://bob.example.com/, and his Atom endpoint is at
http://bob.example.com/atom.cgi. Bob's Atom-enabled client tries to post to
his weblog, by sending an HTTP POST request with his Atom entry:
POST /atom.cgi HTTP/1.1 Host: bob.example.com Content-Type: application/atom+xml <?xml version="1.0" encoding="utf-8"?> <entry xmlns="http://purl.org/atom/ns#"> <title>My Entry Title</title> <created>2003-12-15T14:43:07Z</created> <content type="application/xhtml+xml" xml:lang="en"> <div xmlns="http://www.w3.org/1999/xhtml"> <p>Hello, <em>weblog</em> world!</p> <p>This is my third post <strong>ever</strong>!</p> </div> </content> </entry>
But this request didn't include any authentication information. The server responds
HTTP 401 Unauthorized:
HTTP/1.1 401 Unauthorized WWW-Authenticate: WSSE realm="foo", profile="UsernameToken"
The profile is constant and should always be
"UsernameToken". The realm is
determined by the server and can be anything.
Bob repeats his request, this time with his authentication credentials: username, password digest, nonce, and creation date.
POST /atom.cgi HTTP/1.1
Authorization: WSSE profile="UsernameToken"
X-WSSE: UsernameToken Username="bob", PasswordDigest="quR/EWLAV4xLf9Zqyw4pDmfV9OY=", Nonce="d36e316282959a9ed4c89851497a717f", Created="2003-12-15T14:43:07Z"
<?xml version="1.0" encoding="utf-8"?>
<title>My Entry Title</title>
<content type="application/xhtml+xml" xml:lang="en">
<p>Hello, <em>weblog</em> world!</p>
<p>This is my third post <strong>ever</strong>!</p>
Bob's Atom-enabled weblogging software looks in the
X-WSSE: header for the
actual authentication credentials, and recreates the steps Bob took in order to verify
Bob knows his password. If Bob got his password wrong, the server simply responds
HTTP 401 Unauthorized with the
WWW-Authenticate: header, same as
before; or, optionally, with some explanatory text in the body of the message to tell
client what's going on. If Bob got his password right, the server accepts the request,
the new entry, responds with an
HTTP 201 Created, and gives the location of the
newly created entry.
I want to briefly mention three design decisions and the problems they solve. Remember
I said that CGI scripts couldn't implement HTTP authentication because Apache didn't
along the appropriate headers? The header I was talking about is the
Authorization: header. In this case, Apache will notice the
Authorization: header and notice that the authentication algorithm is "WSSE".
Apache doesn't have a module to handle this, so it will strip the
Authorization: header and pass the rest of the headers (including
X-WSSE:) on to the CGI script.
Second, if Bob has previously successfully authenticated with this same nonce, the server may recognize that and reject the request (with a 401). The server may keep track of nonces for a limited amount of time (usually a matter of minutes) and reject duplicates. If a client tries to reuse a nonce after that time, the server can simply reject it based on the creation timestamp (since the password hash is made from both the nonce and the timestamp). In turn, Bob's Atom-enabled client software generates a new nonce and creation timestamp with each request. This will protect against replay attacks.
Third, this leaves the door open for future versions of Atom supporting other profiles of WSSE, such as Kerberos. But let's jump off that bridge when we come to it.
Optimizing HTTP Authentication
OK, so that's how it looks when it works, and that's how it looks when it doesn't
as you can see, this is an inefficient process. Bob sent his entire entry to the server,
then the server rejected it, then Bob sent his entire entry to the server again --
this time with his WSSE-formatted credentials. But there's nothing about HTTP authentication
or the WSSE Username Token algorithm that requires this extra round trip. If Bob's
Atom-enabled client software knows ahead of time that Bob's Atom-enabled server is
ask for WSSE Username Token authentication, it can simply calculate the credentials
them with the initial request. In the best case, the server sees the credentials,
them, processes the request, and returns the appropriate success code -- all without
generating a 401. In the worst case, the server doesn't actually support WSSE Username
Token, so it simply responds with a 401 and a
WWW-Authenticate: header that
details what algorithms it does support.
More Dive Into XML Columns
Let's make sure we've found a solution that works for Bob. Extending HTTP authentication with WSSE Username Token:
- Doesn't require root access to install Apache modules.
- Doesn't require
.htaccessrights to set up passwords. (The CGI application can manage passwords itself.)
- Doesn't require Bob to have his own web server or even his own IP address. It works with virtual name-based hosting.
- Works with pure-CGI applications. CGI scripts can read the
X-WSSE:header to get the credentials, and they can generate a 401 error and a
WWW-Authenticate:header if authentication fails.
- Doesn't send passwords in the clear. Bob can blog with confidence at O'Reilly conferences.
- Protects against replay attacks.
So that's what Atom authentication looks like and that's why. Because it's the simplest thing that works for Bob.
Disclaimer: the Atom API has not been finalized. While this authentication scheme has been deployed by several vendors, it may still change slightly before the Atom API goes 1.0. Feel free to implement it now, but be prepared to reimplement it later.