web-auth: Authentication Guards for web-server
| (require web-auth) | package: web-auth |
Authentication guards for web-server dispatchers, sized to the patterns small APIs actually ship: a fixed API key (header, with an optional query-parameter escape hatch), bearer tokens backed by a verifier you own, and HTTP Basic. A guard is a procedure from a request to #f (allow) or a response (reject), so it composes with plain dispatchers and stays trivially testable without a server.
1 Guards
procedure
(make-api-key-guard #:verify verify [ #:header header #:query query #:on-unauthorized on-unauthorized]) → guard? verify : (-> string? boolean?) header : string? = "X-API-Key" query : (or/c string? #f) = #f
on-unauthorized : (-> request? response?) = unauthorized-response
Checks header (and, when query is named, that query parameter as an escape hatch). Compare keys with timing-safe-equal? inside verify.
procedure
(make-bearer-guard #:verify verify [ #:realm realm #:on-unauthorized on-unauthorized]) → guard? verify : (-> string? boolean?) realm : string? = "api"
on-unauthorized : (-> request? response?) = unauthorized-response
Parses Authorization: Bearer <token> (scheme matched case-insensitively) and hands the token to verify. Rejections carry WWW-Authenticate: Bearer realm="...".
procedure
(make-basic-auth-guard #:check check [ #:realm realm #:on-unauthorized on-unauthorized]) → guard? check : (-> string? string? boolean?) realm : string? = "restricted"
on-unauthorized : (-> request? response?) = unauthorized-response
Decodes Authorization: Basic <base64> and calls check with user and password. Passwords may contain colons — the split is on the first one.
procedure
(parse-basic-credentials req)
→ (or/c (cons/c string? string?) #f) req : request?
The decoder behind the Basic guard; exported for tests and custom flows.
procedure
v : any/c
Guards are directly applicable procedures.
procedure
(guard-with dispatcher g) → (-> request? response?)
dispatcher : (-> request? response?) g : guard?
Runs g in front of dispatcher; a rejection short-circuits.
2 Building blocks
procedure
(timing-safe-equal? a b) → boolean?
a : (or/c string? bytes?) b : (or/c string? bytes?)
Constant-time content comparison for secrets: the loop folds XOR over every byte and decides at the end. Length inequality still leaks (standard caveat) but content differences do not.
procedure
(request-header req name) → (or/c string? #f)
req : request? name : string?
Case-insensitive header lookup, decoded to a string.
procedure
(query-param req name) → (or/c string? #f)
req : request? name : string?
Query parameter lookup.
procedure
(unauthorized-response [ #:challenge challenge #:body body]) → response? challenge : (or/c bytes? #f) = #f body : bytes? = #"unauthorized"
The default rejection: 401, WWW-Authenticate when a challenge is given, Cache-Control: no-store. Pass #:on-unauthorized to any constructor for custom responses (JSON bodies, redirects, logging).