From c56fb677f8935f025cd7a6ae711fd292cbbb1581 Mon Sep 17 00:00:00 2001
From: "carpentry-heartbeat[bot]"
Date: Thu, 6 Aug 2026 08:52:19 +0200
Subject: [PATCH 1/2] add an RFC 7235 Auth module and Response.unauthorized
http modelled every other major header family but had no authentication
support at all: only Status.unauthorized (401) with a hardcoded reason,
and no way to attach the WWW-Authenticate header that RFC 7235 s3.1
requires of every 401. Consumers (llm, http-client, web) each hand-roll
the header.
Credentials models RFC 7235 s2.1. `credentials` and `challenge` share
an identical ABNF, so one type covers the Authorization and the
WWW-Authenticate side alike: a scheme plus either a token68 or a list
of auth-params. Params are stored as an ordered array of pairs rather
than a Map so that header order survives a parse/serialize round-trip;
Carp's Map iterates by bucket, which would make serialization
non-deterministic.
The list parser is the hard part of RFC 7235: a comma both separates
auth-params within a challenge and separates challenges from each
other. The scanner resolves this with one rule -- a bare token directly
after a scheme is that scheme's token68, and a bare token anywhere else
opens a new challenge -- which is what the grammar implies, since the
token68 alternative can only follow the scheme with no comma between.
Basic (RFC 7617) goes through base64.carp; Bearer (RFC 6750) is plain.
Scheme matching is case-insensitive, the scheme itself is preserved as
written so round-trips are byte-exact, and param values are always
quoted on the way out, which the grammar permits for any of them.
The parser handles attacker-controlled header values, so the scanner
never indexes past the value it was given: quoted-strings that are
unterminated or end in a backslash run to the end of the input, bytes
that fit the grammar nowhere are dropped, and base64 that is invalid or
has no colon comes back as an error.
56 new assertions, 256 total.
---
README.md | 26 ++++
docs/Response.html | 24 ++++
http.carp | 293 ++++++++++++++++++++++++++++++++++++++++++-
test/http.carp | 300 ++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 641 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 91bdc3e..7846c41 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,30 @@ chunk framing. Detect it with `chunked?` and decode it with
_ ())
```
+### Authentication
+
+`Auth` parses and builds the RFC 7235 headers: the credentials a client sends in
+`Authorization`, and the challenges a server answers a 401 with in
+`WWW-Authenticate`. One `WWW-Authenticate` value may carry several challenges.
+
+```clojure
+(match (Auth.parse "Basic YWxhZGRpbjpvcGVuIHNlc2FtZQ==")
+ (Result.Success c)
+ (match (Auth.basic-credentials &c)
+ (Result.Success up) (println* (Pair.a &up) ":" (Pair.b &up))
+ (Result.Error e) (IO.errorln &e))
+ (Result.Error e) (IO.errorln &e))
+
+(Auth.basic "aladdin" "open sesame")
+; => (Success "Basic YWxhZGRpbjpvcGVuIHNlc2FtZQ==")
+(Auth.bearer "mF_9.B5f-4.1JqM") ; => "Bearer mF_9.B5f-4.1JqM"
+
+(let [cs (Auth.parse-challenges "Basic realm=\"a\", Digest realm=\"b\"")]
+ (println* (Credentials.realm (Array.unsafe-nth &cs 1)))) ; => (Just "b")
+
+(Response.unauthorized (Auth.basic-challenge "WallyWorld") {} @"go away")
+```
+
### Status codes
```clojure
@@ -88,6 +112,8 @@ Status.not-found ; => 404
| `Status` | Status code constants and reason phrases |
| `Form` | URL-encoded form body parser |
| `MediaType` | `Content-Type` / media-type parser (type, subtype, parameters) |
+| `Auth` | `Authorization` / `WWW-Authenticate` parser and builder (RFC 7235) |
+| `Credentials` | one authentication scheme with its token68 or auth-params |
| `Multipart` | `multipart/form-data` body decoder |
| `FormPart` | a single decoded multipart part (name, filename, content-type, body) |
| `TransferEncoding` | Chunked transfer-encoding decoder |
diff --git a/docs/Response.html b/docs/Response.html
index 8fdbf8c..b729531 100644
--- a/docs/Response.html
+++ b/docs/Response.html
@@ -793,6 +793,30 @@
+
+
+
+ unauthorized
+
+
+
+ defn
+
+
+ (Fn [String, (Map String (Array String)), String] Response)
+
+
+ (unauthorized challenge headers body)
+
+
+
builds a 401 Unauthorized response carrying challenge
+as its WWW-Authenticate header, which RFC 7235 §3.1 requires of every 401,
+with headers and body. A WWW-Authenticate in headers is replaced. Build
+challenge with Auth.basic-challenge or
+Credentials.str.
+
+
+