web-auth:   Authentication Guards for web-server
1 Guards
make-api-key-guard
make-bearer-guard
make-basic-auth-guard
parse-basic-credentials
guard?
guard-with
2 Building blocks
timing-safe-equal?
request-header
query-param
unauthorized-response
9.3

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

(guard? v) → boolean?

  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).