A batteries included HTTP/1.1 client in OCaml
at main 222 lines 9.0 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** HTTP authentication mechanisms 7 8 This module provides authentication schemes for HTTP requests: 9 10 - {b Basic}: {{:https://datatracker.ietf.org/doc/html/rfc7617}RFC 7617} - Base64 username:password 11 - {b Bearer}: {{:https://datatracker.ietf.org/doc/html/rfc6750}RFC 6750} - OAuth 2.0 tokens 12 - {b Digest}: {{:https://datatracker.ietf.org/doc/html/rfc7616}RFC 7616} - Challenge-response with MD5/SHA-256 13 - {b Signature}: {{:https://datatracker.ietf.org/doc/html/rfc9421}RFC 9421} - HTTP Message Signatures 14 15 For OAuth 2.0 with automatic token refresh, see the [requests.oauth] subpackage. 16 17 {2 Security} 18 19 Per {{:https://datatracker.ietf.org/doc/html/rfc7617#section-4}RFC 7617 Section 4} and 20 {{:https://datatracker.ietf.org/doc/html/rfc6750#section-5.1}RFC 6750 Section 5.1}, 21 Basic, Bearer, and Digest authentication transmit credentials that MUST be 22 protected by TLS. The library enforces HTTPS by default for these schemes. *) 23 24(** Log source for authentication operations *) 25val src : Logs.Src.t 26 27type t 28(** Abstract authentication type *) 29 30val none : t 31(** No authentication *) 32 33val basic : username:string -> password:string -> t 34(** HTTP Basic authentication *) 35 36val bearer : token:string -> t 37(** Bearer token authentication (e.g., OAuth 2.0) *) 38 39val digest : username:string -> password:string -> t 40(** HTTP Digest authentication (RFC 7616). 41 42 Digest authentication is automatically handled: when a request returns 43 a 401 response with a WWW-Authenticate: Digest header, the library will 44 parse the challenge and retry the request with proper digest credentials. 45 46 Supports: 47 - Algorithms: MD5, SHA-256, SHA-512 (not SHA-512-256) 48 - QoP: auth, auth-int (body hashing) 49 - userhash parameter (username hashing) 50 51 Note: SHA-512-256 is not supported as it requires special initialization 52 vectors not available in standard libraries. *) 53 54val signature : Signature.config -> t 55(** HTTP Message Signatures (RFC 9421). 56 57 Creates cryptographic signatures over HTTP message components. 58 The signature covers selected headers and derived values like 59 the method, path, and authority. 60 61 Use {!val:Signature.config} to create the configuration: 62 {[ 63 let key = Signature.Key.ed25519 ~priv:... ~pub:... in 64 let config = Signature.config ~key ~keyid:"my-key" () in 65 let auth = Auth.signature config 66 ]} 67 68 The signature is computed and added when the request is made, 69 as it requires the full request context (method, URI, headers). *) 70 71val bearer_form : token:string -> t 72(** Bearer token in form-encoded body (RFC 6750 Section 2.2). 73 74 This sends the Bearer token as an "access_token" form parameter 75 instead of in the Authorization header. Less preferred than the 76 header method but required by some APIs. 77 78 When using this, set Content-Type to application/x-www-form-urlencoded 79 and use {!get_bearer_form_body} to get the body content. *) 80 81val custom : (Headers.t -> Headers.t) -> t 82(** Custom authentication handler *) 83 84val apply : t -> Headers.t -> Headers.t 85(** Apply authentication to headers. 86 Note: This does not validate transport security. Use [apply_secure] for 87 HTTPS enforcement per RFC 7617/6750. *) 88 89val apply_secure : ?allow_insecure_auth:bool -> url:string -> t -> Headers.t -> Headers.t 90(** Apply authentication with HTTPS validation. 91 Per RFC 7617 Section 4 (Basic) and RFC 6750 Section 5.1 (Bearer): 92 Basic, Bearer, and Digest authentication MUST be used over TLS. 93 94 @param allow_insecure_auth If [true], skip the HTTPS check (not recommended, 95 only for testing environments). Default: [false] 96 @param url The request URL (used for security check) 97 @raise Error.Insecure_auth if sensitive auth is used over HTTP *) 98 99val validate_secure_transport : ?allow_insecure_auth:bool -> url:string -> t -> unit 100(** Validate that sensitive authentication would be safe to use. 101 Raises [Error.Insecure_auth] if Basic/Bearer/Digest auth would be used over HTTP. 102 103 @param allow_insecure_auth If [true], skip the check. Default: [false] *) 104 105val requires_https : t -> bool 106(** Returns [true] if the authentication type requires HTTPS transport. 107 Basic, Bearer, and Digest require HTTPS; No_auth and Custom do not. *) 108 109(** {1 Digest Authentication Support} *) 110 111(** Digest authentication challenge parsed from WWW-Authenticate header *) 112type digest_challenge = { 113 realm : string; 114 nonce : string; 115 qop : string option; 116 algorithm : string; (** MD5, SHA-256, etc. *) 117 opaque : string option; 118 stale : bool; 119 (** If true, the nonce is stale but credentials are valid. Client should 120 retry with the new nonce. Per RFC 7616 Section 3.2.2. *) 121 userhash : bool; 122 (** If true, the server wants the username to be hashed. 123 Per RFC 7616 Section 3.4.4. *) 124} 125 126val parse_www_authenticate : string -> digest_challenge option 127(** [parse_www_authenticate header] parses a WWW-Authenticate header value 128 and returns the Digest challenge if present. Returns [None] if the header 129 is not a Digest challenge or cannot be parsed. *) 130 131(** {2 Nonce Count Tracking} 132 133 Per RFC 7616, the nonce count (nc) must be incremented for each request 134 using the same server nonce to prevent replay attacks. *) 135 136module Nonce_counter : sig 137 type t 138 (** Mutable nonce count tracker, keyed by server nonce *) 139 140 val create : unit -> t 141 (** Create a new nonce counter *) 142 143 val next : t -> nonce:string -> string 144 (** [next t ~nonce] gets and increments the count for the given server nonce. 145 Returns the count formatted as 8 hex digits (e.g., "00000001"). *) 146 147 val clear : t -> unit 148 (** Clear all tracked nonces (e.g., on session reset) *) 149end 150 151val apply_digest : 152 ?nonce_counter:Nonce_counter.t -> 153 ?body:string -> 154 username:string -> 155 password:string -> 156 method_:string -> 157 uri:string -> 158 challenge:digest_challenge -> 159 Headers.t -> 160 Headers.t 161(** [apply_digest ?nonce_counter ?body ~username ~password ~method_ ~uri ~challenge headers] 162 applies Digest authentication to [headers] using the given credentials 163 and server challenge. 164 165 @param nonce_counter Optional nonce counter for replay protection. 166 When provided, the nonce count is tracked and incremented per-nonce 167 across multiple requests in a session. When not provided, defaults 168 to "00000001" (suitable for single-request/one-shot mode). 169 @param body Optional request body for auth-int qop support. 170 When provided and the server supports auth-int qop, the body hash 171 is included in the digest calculation per RFC 7616. *) 172 173val is_digest : t -> bool 174(** [is_digest auth] returns [true] if [auth] is Digest authentication. *) 175 176val get_digest_credentials : t -> (string * string) option 177(** [get_digest_credentials auth] returns [Some (username, password)] if 178 [auth] is Digest authentication, [None] otherwise. *) 179 180val is_bearer_form : t -> bool 181(** [is_bearer_form auth] returns [true] if [auth] is Bearer form authentication. *) 182 183val get_bearer_form_body : t -> string option 184(** [get_bearer_form_body auth] returns [Some "access_token=<token>"] if 185 [auth] is Bearer form authentication, [None] otherwise. 186 Use this to get the form-encoded body content for RFC 6750 Section 2.2. *) 187 188val digest_is_stale : digest_challenge -> bool 189(** [digest_is_stale challenge] returns [true] if the challenge has stale=true. 190 Per RFC 7616 Section 3.2.2: If stale=true, the nonce is expired but the 191 credentials are still valid. The client should retry with the same 192 credentials using the new nonce. If stale=false or not present, the 193 credentials themselves are wrong. *) 194 195(** {1 HTTP Message Signatures (RFC 9421)} *) 196 197val is_signature : t -> bool 198(** [is_signature auth] returns [true] if [auth] is HTTP Message Signature authentication. *) 199 200val get_signature_config : t -> Signature.config option 201(** [get_signature_config auth] returns [Some config] if [auth] is HTTP Message 202 Signature authentication, [None] otherwise. *) 203 204val apply_signature : 205 clock:_ Eio.Time.clock -> 206 method_:Method.t -> 207 uri:Uri.t -> 208 headers:Headers.t -> 209 t -> 210 Headers.t 211(** [apply_signature ~clock ~method_ ~uri ~headers auth] applies HTTP Message Signature 212 to [headers] if [auth] is Signature authentication. Returns the headers with 213 [Signature-Input] and [Signature] headers added. 214 215 This function computes the signature based on the request context and adds 216 the appropriate headers per RFC 9421. 217 218 @param clock Eio clock for timestamp generation in the signature. 219 220 If [auth] is not Signature authentication, returns [headers] unchanged. 221 If signature computation fails, logs an error and returns [headers] unchanged. *) 222