billpg.WWWAuthenticateTools
0.1.0
dotnet add package billpg.WWWAuthenticateTools --version 0.1.0
NuGet\Install-Package billpg.WWWAuthenticateTools -Version 0.1.0
<PackageReference Include="billpg.WWWAuthenticateTools" Version="0.1.0" />
<PackageVersion Include="billpg.WWWAuthenticateTools" Version="0.1.0" />
<PackageReference Include="billpg.WWWAuthenticateTools" />
paket add billpg.WWWAuthenticateTools --version 0.1.0
#r "nuget: billpg.WWWAuthenticateTools, 0.1.0"
#:package billpg.WWWAuthenticateTools@0.1.0
#addin nuget:?package=billpg.WWWAuthenticateTools&version=0.1.0
#tool nuget:?package=billpg.WWWAuthenticateTools&version=0.1.0
WWWAuthenticateTools
Parser and Builder classes for dealing with the WWW-Authenticate HTTP header.
๐ค Why This Exists
The WWW-Authenticate grammar is more subtle than it looks. A single header
value can carry multiple challenges, comma-separated โ but the auth-params
within a challenge are also comma-separated, so telling "another param for
this challenge" from "a new challenge starts here" requires a two-token
lookahead, not a simple Split(','). On top of that, WWW-Authenticate is
one of the header fields RFC 9110/7230 explicitly says can't always be
safely combined into one line by joining multiple header field instances
with commas โ so a correct parser has to accept a collection of raw header
values, not a single pre-joined string. It's easy to write something that
handles the common case and quietly breaks on real-world multi-challenge or
multi-instance headers, which is how this problem ends up solved badly,
piecemeal, in thousands of codebases instead of correctly, once.
Existing libraries were surveyed and found wanting:
- Python's
www-authenticate(PyPI): parses to anOrderedDict, has no generator, uses fragile comma-splitting, and has been unmaintained since ~2014. - npm's
www-authenticate: explicitly documents that it does not support headers with more than one challenge. - .NET's built-in
AuthenticationHeaderValue: models a single scheme plus one opaque parameter string; it doesn't decompose multi-challenge headers at all.
None of them combine correct multi-challenge parsing, a generator, and a design that ports cleanly across languages.
This is the C# port of a planned cross-language library (C#/Python/TypeScript)
for parsing and generating WWW-Authenticate, Proxy-Authenticate,
Authorization, and Proxy-Authorization header values, per RFC 9110
ยง11.6.1 and ยง11.3. The library works purely at the string level โ no
dependency on any HTTP framework.
๐๏ธ Project Layout
billpg.WWWAuthenticateTools/โ the library itself. Target framework isnetstandard2.0for broad compatibility.billpg.WWWAuthenticateToolsTests/โ MSTest unit tests. Per project convention, test assemblies are namedbillpg.(project)Tests.billpg.WWWAuthenticateTools/billpg.WWWAuthenticateTools.slnxโ the solution file, referencing both projects.test-vectors/โ git submodule pointing at www-authenticate-test-vectors, the shared cross-language test vector set. Pinned to a tag, not tracking itsmainbranch.
๐ Getting Started
git submodule update --init --recursive # first time after cloning
dotnet build billpg.WWWAuthenticateTools/billpg.WWWAuthenticateTools.slnx
dotnet test billpg.WWWAuthenticateTools/billpg.WWWAuthenticateTools.slnx
๐ฆ API Overview
The data model is immutable throughout: Challenge (one auth-scheme plus
either a token68 or an ordered list of name/value params) and AuthHeaders
(an ordered collection of Challenge).
Build headers with the fluent builder โ each call returns a new instance, and
WithParam/WithToken68 apply to whichever scheme was added most recently:
var auth = new AuthHeaders()
.WithScheme("HashBack")
.WithParam("version", "RFC12345")
.WithScheme("Basic")
.WithParam("realm", "example");
Generate header text from a model:
auth.ToSingleHeaderValue(); // "HashBack version=RFC12345, Basic realm=example"
auth.ToHeaderLines(); // one string per challenge
Parse raw header values back into a model. The entry point takes one string per actual header line received (not a single pre-joined string), since WWW-Authenticate is one of the header fields that can't always be safely combined by comma-joining multiple instances:
var parsed = ParseHeader.Parse(["Basic realm=example"], strict: true);
โ ๏ธ Exceptions
Every exception carries a stable string Code, which is the cross-language
contract other ports of this library will also use (message text is not):
AuthHeaderExceptionโ abstract base.AuthHeaderParseExceptionโ malformed input during parsing; also carriesHeaderLineIndexandCharacterPosition.AuthHeaderBuilderExceptionโ invalid use of the fluent builder.
The documented codes live in AuthHeaderErrorCodes: no_current_scheme,
duplicate_param, token68_param_conflict, invalid_token68,
invalid_auth_param, unterminated_quoted_string, unexpected_comma.
๐ง Status
Implemented: the data model, immutable fluent builder, exception taxonomy,
generator, a strict-mode parser for RFC-conformant input, one
lenient-mode deviation (control characters in quoted-string values โ see
Design Notes below), and the test-vectors submodule wired into
TestVectorTests.cs
(conformant, lenient, and invalid category, parse-direction vectors).
Not yet implemented:
- The rest of lenient-mode parsing โ every other documented deviation still behaves identically to strict mode.
direction: buildvectors (builder call sequences) from the shared suite.- Python and TypeScript ports.
๐ฏ Design Notes
- Params are kept in an ordered list, not a dictionary, so round-tripping preserves the original order even though lookups are logically case-insensitive by name.
- The builder does not re-validate RFC token grammar (that's the parser's
job for untrusted input) โ it only enforces structural invariants: a
scheme must exist before
WithParam/WithToken68, param names must be unique per challenge, andWithParam/WithToken68are mutually exclusive on the same challenge. - Parsing a challenge's second word (right after the scheme) is ambiguous
between a
token68and the firstauth-paramโ e.g.Bearer abc123==vsDigest realm=example. The parser resolves this the same way for every subsequent comma-separated item too: an auth-param is alwaystoken BWS "=" BWS value, so anything that isn't genuinely shaped that way (including a token68's own trailing==padding, which isn't a realname=valuesplit) is treated as a new challenge or a token68 instead. - A control character (other than HTAB) inside a quoted-string value isn't
valid per RFC 9110 ยง5.6.4's
qdtext/quoted-pairgrammar, raw or backslash-escaped.strict: truerejects it (invalid_auth_param);strict: falsesubstitutes a space rather than passing it through โ otherwise a value containing a raw CR/LF, round-tripped throughGenerate, could smuggle an extra header into the output stream.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Collections.Immutable (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 47 | 9/4/2026 |