OAuth 2.0 authorization and token exchange
0
fork

Configure Feed

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

Add exchange_code and refresh_token that POST over TLS

The library now depends on requests and performs the token endpoint
POST itself, enforcing TLS transport for client_secret. The old
exchange_form_body and refresh_form_body are kept as low-level
primitives but documented to prefer the new functions.

ocaml-auth's exchange_code now delegates to Oauth.exchange_code.

+86 -28
+2
dune-project
··· 23 23 (digestif (>= 1.0)) 24 24 (base64 (>= 3.0)) 25 25 (eqaf (>= 0.9)) 26 + (requests (>= 0.1.0)) 27 + (http (>= 0.1.0)) 26 28 (ohex (>= 0.2)) 27 29 (logs (>= 0.7)) 28 30 (alcotest :with-test)
+2
lib/dune
··· 9 9 digestif 10 10 base64 11 11 eqaf 12 + requests 13 + http 12 14 fmt 13 15 ohex 14 16 logs))
+32
lib/oauth.ml
··· 314 314 | Missing_access_token 315 315 | Invalid_token_format 316 316 | Unsupported_token_type of string 317 + | Http_error of int 317 318 318 319 let pp_parse_token_error fmt = function 319 320 | Invalid_json -> Fmt.pf fmt "Invalid JSON" ··· 321 322 | Invalid_token_format -> Fmt.pf fmt "Invalid token format" 322 323 | Unsupported_token_type t -> 323 324 Fmt.pf fmt "Unsupported token_type %S (only Bearer is supported)" t 325 + | Http_error status -> Fmt.pf fmt "HTTP error %d from token endpoint" status 324 326 325 327 let has_substring ~sub s = 326 328 let len = String.length sub in ··· 380 382 ("client_secret", client_secret); 381 383 ("refresh_token", refresh_token); 382 384 ] 385 + 386 + let token_headers = 387 + Http.Headers.of_list 388 + [ 389 + ("Content-Type", "application/x-www-form-urlencoded"); 390 + ("Accept", "application/json"); 391 + ] 392 + 393 + let post_token_endpoint http provider form_str = 394 + let url = token_url provider in 395 + let body = Requests.Body.text form_str in 396 + let resp = Requests.post http url ~body ~headers:token_headers in 397 + let status = Requests.Response.status_code resp in 398 + if status >= 400 then begin 399 + Log.warn (fun m -> m "Token endpoint returned HTTP %d" status); 400 + Error (Http_error status) 401 + end 402 + else parse_token_response (Requests.Response.text resp) 403 + 404 + let exchange_code http provider ~client_id ~client_secret ~code ~redirect_uri 405 + ?code_verifier () = 406 + let form_str = 407 + exchange_form_body ~client_id ~client_secret ~code ~redirect_uri 408 + ?code_verifier () 409 + in 410 + post_token_endpoint http provider form_str 411 + 412 + let refresh_token http provider ~client_id ~client_secret ~refresh_token = 413 + let form_str = refresh_form_body ~client_id ~client_secret ~refresh_token in 414 + post_token_endpoint http provider form_str 383 415 384 416 (* ── Userinfo Parsing ────────────────────────────────────────────── *) 385 417
+48 -28
lib/oauth.mli
··· 222 222 223 223 (** {1:exchange Token Exchange} *) 224 224 225 - val exchange_form_body : 226 - client_id:string -> 227 - client_secret:string -> 228 - code:string -> 229 - redirect_uri:redirect_uri -> 230 - ?code_verifier:string -> 231 - unit -> 232 - string 233 - (** [exchange_form_body ~client_id ~client_secret ~code ~redirect_uri 234 - ?code_verifier ()] is an [application/x-www-form-urlencoded] string for 235 - exchanging an authorization code for an access token (RFC 6749 §4.1.3). 236 - 237 - When [~code_verifier] is provided the [code_verifier] parameter is included 238 - per RFC 7636 §4.5. 239 - 240 - {b Security}: This body contains [client_secret] in cleartext. It must only 241 - be sent over TLS to the provider's token endpoint (RFC 6749 §3.2). *) 242 - 243 - (** {1:token Token Response} *) 244 - 245 225 type token_response = { 246 226 access_token : string; 247 227 expires_in : int option; 248 228 refresh_token : string option; 249 229 refresh_token_expires_in : int option; 250 230 } 251 - (** Parsed token response. *) 231 + (** Parsed token response. Only Bearer tokens are accepted. *) 252 232 253 233 type parse_token_error = 254 234 | Invalid_json ··· 257 237 | Unsupported_token_type of string 258 238 (** The server returned a [token_type] other than ["bearer"]. This library 259 239 only supports Bearer tokens (RFC 6750). *) 240 + | Http_error of int (** The token endpoint returned an HTTP error status. *) 241 + 242 + val exchange_code : 243 + Requests.t -> 244 + provider -> 245 + client_id:string -> 246 + client_secret:string -> 247 + code:string -> 248 + redirect_uri:redirect_uri -> 249 + ?code_verifier:string -> 250 + unit -> 251 + (token_response, parse_token_error) result 252 + (** [exchange_code http provider ~client_id ~client_secret ~code ~redirect_uri 253 + ?code_verifier ()] exchanges an authorization code for an access token by 254 + POSTing to the provider's token endpoint over TLS (RFC 6749 §4.1.3). 255 + 256 + When [~code_verifier] is provided, it is included per RFC 7636 §4.5. *) 257 + 258 + val refresh_token : 259 + Requests.t -> 260 + provider -> 261 + client_id:string -> 262 + client_secret:string -> 263 + refresh_token:string -> 264 + (token_response, parse_token_error) result 265 + (** [refresh_token http provider ~client_id ~client_secret ~refresh_token] 266 + refreshes an access token by POSTing to the provider's token endpoint over 267 + TLS (RFC 6749 §6). *) 260 268 261 269 val parse_token_response : string -> (token_response, parse_token_error) result 262 - (** [parse_token_response body] parses a JSON token response. *) 270 + (** [parse_token_response body] parses a JSON token response. Prefer 271 + {!exchange_code} and {!refresh_token} which handle the HTTP transport. *) 263 272 264 273 val pp_parse_token_error : Format.formatter -> parse_token_error -> unit 265 274 266 - (** {1:refresh Token Refresh} *) 275 + (** {2 Low-level Form Encoding} 276 + 277 + These produce raw form bodies. Prefer {!exchange_code} and {!refresh_token} 278 + which handle TLS transport. *) 279 + 280 + val exchange_form_body : 281 + client_id:string -> 282 + client_secret:string -> 283 + code:string -> 284 + redirect_uri:redirect_uri -> 285 + ?code_verifier:string -> 286 + unit -> 287 + string 288 + (** [exchange_form_body] produces the form-encoded body for a token exchange. 289 + Contains [client_secret] in cleartext — use {!exchange_code} instead. *) 267 290 268 291 val refresh_form_body : 269 292 client_id:string -> client_secret:string -> refresh_token:string -> string 270 - (** [refresh_form_body ~client_id ~client_secret ~refresh_token] is a 271 - form-encoded string for refreshing an access token (RFC 6749 §6). 272 - 273 - {b Security}: This body contains [client_secret] in cleartext. It must only 274 - be sent over TLS to the provider's token endpoint (RFC 6749 §3.2). *) 293 + (** [refresh_form_body] produces the form-encoded body for a token refresh. 294 + Contains [client_secret] in cleartext — use {!refresh_token} instead. *) 275 295 276 296 (** {1:userinfo Userinfo Parsing} *) 277 297
+2
oauth.opam
··· 19 19 "digestif" {>= "1.0"} 20 20 "base64" {>= "3.0"} 21 21 "eqaf" {>= "0.9"} 22 + "requests" {>= "0.1.0"} 23 + "http" {>= "0.1.0"} 22 24 "ohex" {>= "0.2"} 23 25 "logs" {>= "0.7"} 24 26 "alcotest" {with-test}