OAuth 2.0 authorization and token exchange
0
fork

Configure Feed

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

oauth: strip UTF-8 from lib comments and docstrings

Replace section marks, en/em dashes, ellipsis, and smart quotes with
ASCII equivalents across oauth.ml and oauth.mli. This aligns the file
with the project's ASCII-only comment rule. No semantic change; the
only non-comment edit is in a couple of error strings (replacing "§"
with "section" inside quoted messages).

+54 -46
+13 -9
lib/oauth.ml
··· 17 17 par_endpoint : string option; 18 18 } 19 19 20 - (* Sanitize a string for use as a URL path segment per RFC 3986 §3.3: 20 + (* Sanitize a string for use as a URL path segment per RFC 3986 section 3.3: 21 21 lowercase, keep only unreserved chars [a-z0-9-], collapse runs of 22 22 dashes, strip leading/trailing dashes. Non-ASCII bytes (UTF-8) are 23 23 treated as separators (slugified), not percent-encoded, to keep ··· 170 170 match Uri.fragment uri with 171 171 | Some _ -> 172 172 Error 173 - (`Msg "redirect_uri must not contain a fragment (RFC 6749 §3.1.2)") 173 + (`Msg 174 + "redirect_uri must not contain a fragment (RFC 6749 section \ 175 + 3.1.2)") 174 176 | None -> Ok s) 175 177 | Some "http" when is_loopback_http uri -> ( 176 178 match Uri.fragment uri with 177 179 | Some _ -> 178 180 Error 179 - (`Msg "redirect_uri must not contain a fragment (RFC 6749 §3.1.2)") 181 + (`Msg 182 + "redirect_uri must not contain a fragment (RFC 6749 section \ 183 + 3.1.2)") 180 184 | None -> Ok s) 181 185 | Some "http" -> 182 186 Error ··· 203 207 type challenge_method = S256 | Plain 204 208 type code_verifier = string 205 209 206 - (* Base64url encoding per RFC 4648 §5, no padding. *) 210 + (* Base64url encoding per RFC 4648 section 5, no padding. *) 207 211 let base64url_encode_no_pad s = 208 212 let b64 = Base64.encode_exn ~pad:false ~alphabet:Base64.uri_safe_alphabet s in 209 213 b64 ··· 225 229 let code_verifier_to_string s = s 226 230 227 231 let generate_code_verifier () = 228 - (* 32 random bytes -> 43 base64url chars (RFC 7636 §4.1) *) 232 + (* 32 random bytes -> 43 base64url chars (RFC 7636 section 4.1) *) 229 233 base64url_encode_no_pad (Crypto_rng.generate 32) 230 234 231 235 let code_challenge method_ verifier = 232 236 match method_ with 233 237 | Plain -> verifier 234 238 | S256 -> 235 - (* BASE64URL(SHA256(ASCII(code_verifier))) per RFC 7636 §4.2 *) 239 + (* BASE64URL(SHA256(ASCII(code_verifier))) per RFC 7636 section 4.2 *) 236 240 let hash = Digestif.SHA256.(digest_string verifier |> to_raw_string) in 237 241 base64url_encode_no_pad hash 238 242 ··· 273 277 274 278 (* RFC 3986 unreserved set. Shared by the authorization URL builder, the 275 279 token-endpoint form body, and Basic-auth credential encoding (RFC 6749 276 - §2.3.1 says credentials use application/x-www-form-urlencoded, which is 280 + section 2.3.1 says credentials use application/x-www-form-urlencoded, which is 277 281 this same set on the byte level). *) 278 282 let pct_encode s = 279 283 let buf = Buffer.create (String.length s) in ··· 311 315 [refresh_token]) to [form_fields] before form-encoding. [client_id] is 312 316 always included in the body because several providers require it in 313 317 both the header and the form; putting it there is safe as [client_id] 314 - is not a secret (RFC 6749 §2.3.1). *) 318 + is not a secret (RFC 6749 section 2.3.1). *) 315 319 let apply = function 316 320 | None { client_id } -> ([ ("client_id", client_id) ], []) 317 321 | Post { client_id; client_secret } -> 318 322 ([ ("client_id", client_id); ("client_secret", client_secret) ], []) 319 323 | Basic { client_id; client_secret } -> 320 - (* RFC 6749 §2.3.1: percent-encode both halves before joining by ':' 324 + (* RFC 6749 section 2.3.1: percent-encode both halves before joining by ':' 321 325 to avoid ambiguity with secrets containing colons or non-ASCII. *) 322 326 let cred = 323 327 Printf.sprintf "%s:%s" (pct_encode client_id)
+41 -37
lib/oauth.mli
··· 74 74 Fields are readable for pattern matching. 75 75 76 76 {b Security}: All URLs must use HTTPS. Per 77 - {{:https://datatracker.ietf.org/doc/html/rfc6749#section-3.1} RFC 6749 §3.1} 78 - the authorization endpoint must use TLS, and per 79 - {{:https://datatracker.ietf.org/doc/html/rfc6749#section-3.2} §3.2} the 80 - token endpoint must use TLS. Use {!custom_provider} to construct values with 81 - HTTPS validation. *) 77 + {{:https://datatracker.ietf.org/doc/html/rfc6749#section-3.1} RFC 6749 78 + section 3.1} the authorization endpoint must use TLS, and per 79 + {{:https://datatracker.ietf.org/doc/html/rfc6749#section-3.2} section 3.2} 80 + the token endpoint must use TLS. Use {!custom_provider} to construct values 81 + with HTTPS validation. *) 82 82 83 83 val custom_provider : 84 84 name:string -> ··· 93 93 ?par_endpoint ()] constructs a custom provider configuration after 94 94 validating that: 95 95 - All endpoint URLs (including [par_endpoint] if supplied) use HTTPS (RFC 96 - 6749 §3.1–3.2, RFC 9126 §2). 96 + 6749 section 3.1-3.2, RFC 9126 section 2). 97 97 - The slug derived from [name] does not collide with a built-in provider 98 98 (["github"], ["google"], ["gitlab"]), which would make callback routes 99 99 ambiguous. ··· 139 139 unvalidated, potentially user-controlled strings as the OAuth redirect 140 140 target 141 141 ({{:https://datatracker.ietf.org/doc/html/rfc6749#section-10.15} RFC 6749 142 - §10.15}). *) 142 + section 10.15}). *) 143 143 144 144 val redirect_uri : string -> (redirect_uri, [ `Msg of string ]) result 145 145 (** [redirect_uri s] validates [s] as an OAuth redirect URI. ··· 148 148 - HTTPS scheme, or [http://localhost] / [http://127.0.0.1] for native app 149 149 development per 150 150 {{:https://datatracker.ietf.org/doc/html/rfc8252#section-7.3} RFC 8252 151 - §7.3}. 151 + section 7.3}. 152 152 - No fragment component 153 153 ({{:https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2} RFC 6749 154 - §3.1.2}). 154 + section 3.1.2}). 155 155 - Non-empty path. 156 156 157 157 Returns [Error (`Msg reason)] if validation fails. *) ··· 189 189 Code Exchange mitigates authorization code interception attacks against 190 190 public clients. *) 191 191 192 - (** Code challenge method per RFC 7636 §4.2. *) 192 + (** Code challenge method per RFC 7636 section 4.2. *) 193 193 type challenge_method = 194 194 | S256 (** SHA-256 transform (recommended). *) 195 195 | Plain (** Plain text (only when S256 is unsupported by the server). *) ··· 200 200 201 201 val generate_code_verifier : unit -> code_verifier 202 202 (** [generate_code_verifier ()] generates a code verifier with 32 random bytes 203 - (43 base64url characters), satisfying RFC 7636 §4.1. 203 + (43 base64url characters), satisfying RFC 7636 section 4.1. 204 204 205 205 @raise Crypto_rng.Unseeded_generator if the RNG is not initialized. *) 206 206 ··· 211 211 val code_verifier_of_string : 212 212 string -> (code_verifier, [ `Msg of string ]) result 213 213 (** [code_verifier_of_string s] validates [s] as a PKCE code verifier per RFC 214 - 7636 §4.1: 43–128 characters from the unreserved set [[A-Za-z0-9._~-]]. *) 214 + 7636 section 4.1: 43-128 characters from the unreserved set 215 + [[A-Za-z0-9._~-]]. *) 215 216 216 217 val code_challenge : challenge_method -> code_verifier -> string 217 218 (** [code_challenge method_ verifier] is the [code_challenge] derived from 218 219 [verifier]. For [S256] this is [BASE64URL(SHA256(verifier))]; for [Plain] it 219 - is the verifier itself (RFC 7636 §4.2). *) 220 + is the verifier itself (RFC 7636 section 4.2). *) 220 221 221 222 (** {1:authz Authorization URL} *) 222 223 ··· 236 237 omits the parameter. 237 238 238 239 When [~code_challenge] is provided the [code_challenge] and 239 - [code_challenge_method] query parameters are included per RFC 7636 §4.3. 240 - [code_challenge_method] defaults to [S256]. *) 240 + [code_challenge_method] query parameters are included per RFC 7636 section 241 + 4.3. [code_challenge_method] defaults to [S256]. *) 241 242 242 243 (** {1:client_auth Client Authentication} 243 244 244 245 How a confidential client authenticates to the token endpoint. Per 245 - {{:https://datatracker.ietf.org/doc/html/rfc6749#section-2.3} RFC 6749 §2.3}, 246 - servers MAY accept any of several methods; which one a given deployment 247 - requires is provider-specific. This type abstracts the choice so the same 248 - code works for any provider. 246 + {{:https://datatracker.ietf.org/doc/html/rfc6749#section-2.3} RFC 6749 247 + section 2.3}, servers MAY accept any of several methods; which one a given 248 + deployment requires is provider-specific. This type abstracts the choice so 249 + the same code works for any provider. 249 250 250 251 Choose carefully: 251 252 252 253 - [basic] 253 254 ({{:https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1} RFC 6749 254 - §2.3.1}) is the preferred method for confidential clients with a shared 255 - secret. The credentials go in the [Authorization] header, which means they 256 - never hit intermediate proxies as a form parameter. 255 + section 2.3.1}) is the preferred method for confidential clients with a 256 + shared secret. The credentials go in the [Authorization] header, which 257 + means they never hit intermediate proxies as a form parameter. 257 258 - [post] (same section, alt form) places the secret in the request body. 258 259 Still TLS-protected but more likely to leak into logs; use only when the 259 260 provider mandates it (GitHub historically required this). 260 261 - [none] is for public clients (installed apps, SPAs) that cannot keep a 261 262 secret. 262 263 263 - Future variants (not yet implemented): [private_key_jwt] (RFC 7523 §2.2 264 - asymmetric), [client_secret_jwt] (RFC 7523 §2.2 symmetric HMAC). *) 264 + Future variants (not yet implemented): [private_key_jwt] (RFC 7523 section 265 + 2.2 asymmetric), [client_secret_jwt] (RFC 7523 section 2.2 symmetric HMAC). 266 + *) 265 267 266 268 module Client_auth : sig 267 269 type t ··· 270 272 (** Public client. Sends [client_id] in the request body; no credential. *) 271 273 272 274 val basic : client_id:string -> client_secret:string -> t 273 - (** HTTP Basic authentication per RFC 6749 §2.3.1. Emits an 275 + (** HTTP Basic authentication per RFC 6749 section 2.3.1. Emits an 274 276 [Authorization: Basic base64(client_id:client_secret)] header. Both fields 275 - are percent-encoded before joining, per RFC 6749 §2.3.1. *) 277 + are percent-encoded before joining, per RFC 6749 section 2.3.1. *) 276 278 277 279 val post : client_id:string -> client_secret:string -> t 278 - (** Client credentials in the request body per RFC 6749 §2.3.1. Emits 280 + (** Client credentials in the request body per RFC 6749 section 2.3.1. Emits 279 281 [client_id] and [client_secret] as form fields. *) 280 282 281 283 val client_id : t -> string ··· 287 289 [refresh_token]) to [form_fields] before form-encoding. For {!none} and 288 290 {!post} [headers] is empty; for {!basic} it carries 289 291 [Authorization: Basic base64(pct(client_id):pct(client_secret))] per RFC 290 - 6749 §2.3.1. *) 292 + 6749 section 2.3.1. *) 291 293 end 292 294 293 295 (** {1:exchange Token Exchange} *) ··· 320 322 (token_response, parse_token_error) result 321 323 (** [exchange_code http provider ~client_auth ~code ~redirect_uri ?code_verifier 322 324 ()] exchanges an authorization code for an access token by POSTing to the 323 - provider's token endpoint (RFC 6749 §4.1.3). 325 + provider's token endpoint (RFC 6749 section 4.1.3). 324 326 325 327 [client_auth] controls how the client authenticates; see {!Client_auth}. 326 - When [~code_verifier] is provided, it is included per RFC 7636 §4.5. 328 + When [~code_verifier] is provided, it is included per RFC 7636 section 4.5. 327 329 328 330 @raise Invalid_argument 329 331 if [http] has TLS certificate verification disabled. {!Requests.create} ··· 336 338 refresh_token:string -> 337 339 (token_response, parse_token_error) result 338 340 (** [refresh_token http provider ~client_auth ~refresh_token] refreshes an 339 - access token by POSTing to the provider's token endpoint (RFC 6749 §6). 341 + access token by POSTing to the provider's token endpoint (RFC 6749 section 342 + 6). 340 343 341 344 @raise Invalid_argument 342 345 if TLS verification is disabled (same as {!exchange_code}). *) ··· 361 364 362 365 module Par : sig 363 366 type response = { request_uri : string; expires_in : int } 364 - (** Successful PAR response per RFC 9126 §2.2. *) 367 + (** Successful PAR response per RFC 9126 section 2.2. *) 365 368 366 369 type error = 367 370 | No_par_endpoint (** The provider has no configured PAR endpoint. *) ··· 386 389 (response, error) result 387 390 (** [push http provider ~client_auth ~redirect_uri ~state ~scope 388 391 ?code_challenge ?code_challenge_method ?dpop_proof ()] sends the 389 - authorization parameters to the provider's PAR endpoint (RFC 9126 §2.1). 392 + authorization parameters to the provider's PAR endpoint (RFC 9126 section 393 + 2.1). 390 394 391 395 The body carries the same [response_type=code] / [redirect_uri] / [state] 392 396 / [scope] / PKCE parameters as {!authorization_url} would have placed in 393 397 the query string. [client_auth] authenticates the request. 394 398 395 399 If [dpop_proof] is supplied it is sent in the [DPoP] header, binding the 396 - authorization to the client's DPoP key at request time (RFC 9449 §10 when 397 - the server requires DPoP on PAR). 400 + authorization to the client's DPoP key at request time (RFC 9449 section 401 + 10 when the server requires DPoP on PAR). 398 402 399 403 Returns [Error No_par_endpoint] if [provider] has no PAR endpoint. 400 404 ··· 405 409 provider -> client_id:string -> request_uri:string -> string 406 410 (** [authorization_url provider ~client_id ~request_uri] builds the 407 411 authorization-endpoint URL carrying only [client_id] and [request_uri], 408 - per RFC 9126 §4. All other authorization parameters are already stored 409 - server-side under [request_uri]. *) 412 + per RFC 9126 section 4. All other authorization parameters are already 413 + stored server-side under [request_uri]. *) 410 414 411 415 val parse_response : string -> (response, error) result 412 416 (** [parse_response body] parses a PAR server response. *)