I Wrote a NuGet Package!

I’ve been working a little in the world of HTTP Authentication. When you connect a system to another system, there’s usually a set of tokens that get passed around and managed and sometimes accidentally pushed to a public git repo.

When you try to connect to a website that requires a user login, you’ll be sent back a 401 response with a WWW-Authenticate: header. Except you won’t. You’ll be sent a 302 redirect to a login page.

That’s a little unfortunate for anyone doing server-to-server as the 401 response is ideal for APIs. Instead, if you get a 200 response (after you follow the redirect) and the contents looks like HTML with a login form instead of the JSON you were expecting, then your API token must have just expired! (And if you were actually expecting some HTML, good luck!)

"Big man, walking in the park. Wigwam, frightened of the dark."

WWW-Authenticate

Let’s talk about this header. The 401 says you need to be logged in, but the header tells you exactly how you do that. This is known as a “Challenge” in the parlance. The client is being challenged to authenticate their identity somehow. Here’s what one of those headers with a challenge looks like.

WWW-Authenticate: Basic realm="My Web Site"

Looks simple enough. “Basic” means basic user/password style authentication. Your next request includes a username and password in a base64-encoded block. Simple.

The first thing to realize is that HTTP does allow a response to have many headers of the same name. If your HTTP library gives you a function that returns a named header’s single string value, then the designer of that library has made a mistake. A mistake I’ve made myself so you’re in good company.

This does mean that if a server supports many different methods of authentication and lets the client pick one, that response with the many challenges is either going to have to list many of them in a single header or return single challenges in multiple headers.

It means that this header…

WWW-Authenticate: Basic realm="My site"
WWW-Authenticate: Bearer realm="Still my site"
WWW-Authenticate: IMadeItUp realm="No really I did"

and this header…

WWW-Authenticate: Basic realm="My Site", Bearer realm="Still my site", IMadeItUp realm="No really I did"

are completely equivalent.

Both ways are a trade off. Some HTTP libraries simply don’t handle multiple headers with the same name, so the single header with multiple challenges might be preferable, but then that’s a whole other can of worms.

Let’s open the can, but hold on if your can opener is x.Split(',')!

“But what do you mean by comma?”

A surprising amount of HTTP tooling falls apart the moment you look closely at the WWW-Authenticate header. On paper it is very simple. The server lists one or more authentication schemes and the client chooses one. In practice, the header is a tiny grammar with decades of historical baggage, where commas, spaces, and equals signs all change meaning depending on the parsing state. It’s one of those corners of HTTP where the RFC is technically complete but practically opaque. If only JSON was a thing back then.

The core problem is that the header has to express three very different forms. It could be a bare scheme name, or a scheme with a “token68”, or most commonly a scheme with a set of named parameters. And it uses the same punctuation for all of them. A comma might mean “next parameter” or “next scheme”. An equals sign might mean “this is a parameter” or “this is padding at the end of a token68”. A space might be a separator, or it might be meaningless, or it might be forbidden entirely depending on what came before. If you’re thinking of splitting on commas or equals signs, you’re already in trouble.

Let’s look at a worst case scenario…

WWW-Authenticate: Newauth realm="apps, or the whole enterprise", type=1, title="Say \"hello\"", Basic realm="simple", Bearer dXNlcjpwYXNz==, Digest realm="notice, the comma here too", qop="auth, auth-int", algorithm=SHA-256

“So Newauth is the challenge type. There’s a comma inside the realm but it’s inside a quoted string. So we have realmtype, and title. So far so good. Basic followed by a space. That’s a new scheme, or is it a parameter and they’ve put a space before the equals? Let’s read ahead, no it’s definitely a new scheme, keep going. The Bearer scheme has a parameter called dXNlcjpwYXNz with a value =, that’s not allowed it must be a token68 value, keep going…”

The grammar makes things even more interesting because a challenge can contain either a token68 or a set of parameters, but never both. That single rule changes how you interpret every character that follows the scheme name. If the first token after the scheme looks like a token68 and is followed by a comma, that comma must be a scheme separator. If the first token looks like name=value, then every comma that follows might be another parameter, unless it’s actually the start of a new scheme. The meaning of each punctuation mark is contextual, and the context changes as you parse.

This is exactly the kind of problem that looks trivial until you try to write a parser. Once you do, you discover the edge cases. Quoted strings containing commas, token68 values ending in multiple equals signs, malformed headers that need graceful handling, the subtle rule that whitespace is allowed almost everywhere except where it absolutely isn’t. The RFC, to be fair, does answer all these questions, but your boss keeps asking you why a simple split-by-comma parser is taking so long. After implementing all of this myself, I realized there wasn’t a clean, reusable library that handled the full grammar correctly. So I wrote one.

"I swear, sometimes, that man is out to get me."

Downloads: zero

I’ve now published WWWAuthenticateTools on NuGet, a small focused library that can parse and build WWW-Authenticate headers correctly, round-trip them safely, and give you a clean object model instead of a pile of string-splitting heuristics. If you’ve ever had to deal with multiple authentication schemes, custom schemes, or clients that insist on single‑line headers, this library should save you a lot of pain. (It’s still in a pre-release state with a zero-dot version. I’ll change it to one-dot-zero once I’ve had a few people review it.)

This library explicitly avoids integrating with any HTTP libraries. Think of it like your JSON or XML parser. Strings come in, strings go out. I’m planning to write Python and JS/TypeScript ports, but if you want to port it to your favorite ecosystem, please do. If you’ve found a bug or you want to contribute a test vector, please see the project’s Github page.

Picture credits
📸 “Free Hugs” by me.
📸 “Cat on Laptop” by Doug Woods. (Creative Commons)

 

billpg industries

The engineering blog of Bill P. Godfrey.


By Bill Godfrey, 2026-09-05