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