A batteries included HTTP/1.1 client in OCaml
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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