OAuth 2.0 authorization and token exchange
0
fork

Configure Feed

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

oauth: Client_auth type for RFC 6749 §2.3.1 client authentication

Replaces the scattered ~client_id/~client_secret arguments with a single
Client_auth.t value that captures the authentication method. Supports
none (public client), basic (Authorization header, preferred), and post
(credentials in form body). Basic percent-encodes both halves before
joining with ':' to avoid ambiguity with secrets containing ':' or
non-ASCII bytes.

Callers now pass ~client_auth to exchange_code, refresh_token, Token.make
and Token.of_response. Groundwork for upcoming private_key_jwt and
client_secret_jwt variants (RFC 7523).

+268 -101
+4 -1
README.md
··· 48 48 if not (Oauth.validate_state ~expected:state ~actual:callback_state) then 49 49 failwith "CSRF state mismatch"; 50 50 51 + let client_auth = 52 + Oauth.Client_auth.basic ~client_id:"xxx" ~client_secret:"yyy" 53 + in 51 54 match 52 - Oauth.exchange_code http Github ~client_id:"xxx" ~client_secret:"yyy" 55 + Oauth.exchange_code http Github ~client_auth 53 56 ~code ~redirect_uri ~code_verifier:verifier () 54 57 with 55 58 | Ok token -> Printf.printf "Access token: %s\n" token.access_token
+103 -71
lib/oauth.ml
··· 249 249 in 250 250 Uri.with_query uri query |> Uri.to_string 251 251 252 - (* -- Token Exchange ------------------------------------------------ *) 252 + (* -- URL / form encoding ------------------------------------------- *) 253 253 254 + (* RFC 3986 unreserved set. Shared by the authorization URL builder, the 255 + token-endpoint form body, and Basic-auth credential encoding (RFC 6749 256 + §2.3.1 says credentials use application/x-www-form-urlencoded, which is 257 + this same set on the byte level). *) 254 258 let pct_encode s = 255 259 let buf = Buffer.create (String.length s) in 256 260 String.iter ··· 266 270 String.concat "&" 267 271 (List.map (fun (k, v) -> pct_encode k ^ "=" ^ pct_encode v) params) 268 272 269 - let exchange_form_body ~client_id ~client_secret ~code ~redirect_uri 270 - ?code_verifier () = 271 - let base = 273 + (* -- Client Authentication ----------------------------------------- *) 274 + 275 + module Client_auth = struct 276 + type t = 277 + | None of { client_id : string } 278 + | Basic of { client_id : string; client_secret : string } 279 + | Post of { client_id : string; client_secret : string } 280 + 281 + let none ~client_id = None { client_id } 282 + let basic ~client_id ~client_secret = Basic { client_id; client_secret } 283 + let post ~client_id ~client_secret = Post { client_id; client_secret } 284 + 285 + let client_id = function 286 + | None { client_id } | Basic { client_id; _ } | Post { client_id; _ } -> 287 + client_id 288 + 289 + (* [apply auth] yields [(form_fields, headers)] for a token-endpoint 290 + request. Callers append grant-type-specific fields (e.g. [code], 291 + [refresh_token]) to [form_fields] before form-encoding. [client_id] is 292 + always included in the body because several providers require it in 293 + both the header and the form; putting it there is safe as [client_id] 294 + is not a secret (RFC 6749 §2.3.1). *) 295 + let apply = function 296 + | None { client_id } -> ([ ("client_id", client_id) ], []) 297 + | Post { client_id; client_secret } -> 298 + ([ ("client_id", client_id); ("client_secret", client_secret) ], []) 299 + | Basic { client_id; client_secret } -> 300 + (* RFC 6749 §2.3.1: percent-encode both halves before joining by ':' 301 + to avoid ambiguity with secrets containing colons or non-ASCII. *) 302 + let cred = 303 + Printf.sprintf "%s:%s" (pct_encode client_id) 304 + (pct_encode client_secret) 305 + in 306 + let b64 = Base64.encode_exn cred in 307 + ([ ("client_id", client_id) ], [ ("Authorization", "Basic " ^ b64) ]) 308 + end 309 + 310 + let client_auth_apply = Client_auth.apply 311 + 312 + (* -- Token Exchange ------------------------------------------------ *) 313 + 314 + let exchange_form_body ~client_auth ~code ~redirect_uri ?code_verifier () = 315 + let auth_fields, auth_headers = client_auth_apply client_auth in 316 + let grant = 272 317 [ 273 318 ("grant_type", "authorization_code"); 274 - ("client_id", client_id); 275 - ("client_secret", client_secret); 276 319 ("code", code); 277 320 ("redirect_uri", redirect_uri_to_string redirect_uri); 278 321 ] 279 322 in 280 - let params = 281 - match code_verifier with 282 - | None -> base 283 - | Some v -> base @ [ ("code_verifier", v) ] 323 + let extras = 324 + match code_verifier with None -> [] | Some v -> [ ("code_verifier", v) ] 284 325 in 285 - form_encode params 326 + (form_encode (auth_fields @ grant @ extras), auth_headers) 286 327 287 328 (* -- Token Response ------------------------------------------------ *) 288 329 ··· 318 359 refresh_token; 319 360 refresh_token_expires_in; 320 361 }) 321 - |> Object.mem "access_token" string ~enc:(fun t -> 322 - t.access_token) 323 - |> Object.opt_mem "token_type" string ~enc:(fun t -> 324 - t.token_type) 325 - |> Object.opt_mem "expires_in" int ~enc:(fun t -> 326 - t.expires_in) 327 - |> Object.opt_mem "refresh_token" string ~enc:(fun t -> 328 - t.refresh_token) 329 - |> Object.opt_mem "refresh_token_expires_in" int 330 - ~enc:(fun t -> t.refresh_token_expires_in) 362 + |> Object.mem "access_token" string ~enc:(fun t -> t.access_token) 363 + |> Object.opt_mem "token_type" string ~enc:(fun t -> t.token_type) 364 + |> Object.opt_mem "expires_in" int ~enc:(fun t -> t.expires_in) 365 + |> Object.opt_mem "refresh_token" string ~enc:(fun t -> t.refresh_token) 366 + |> Object.opt_mem "refresh_token_expires_in" int ~enc:(fun t -> 367 + t.refresh_token_expires_in) 331 368 |> Object.skip_unknown |> Object.finish 332 369 333 370 type parse_token_error = ··· 396 433 397 434 (* -- Token Refresh ------------------------------------------------- *) 398 435 399 - let refresh_form_body ~client_id ~client_secret ~refresh_token = 400 - form_encode 401 - [ 402 - ("grant_type", "refresh_token"); 403 - ("client_id", client_id); 404 - ("client_secret", client_secret); 405 - ("refresh_token", refresh_token); 406 - ] 436 + let refresh_form_body ~client_auth ~refresh_token = 437 + let auth_fields, auth_headers = client_auth_apply client_auth in 438 + let body = 439 + form_encode 440 + (auth_fields 441 + @ [ ("grant_type", "refresh_token"); ("refresh_token", refresh_token) ]) 442 + in 443 + (body, auth_headers) 407 444 408 - let token_headers = 409 - Http.Headers.of_list 410 - [ 411 - ("Content-Type", "application/x-www-form-urlencoded"); 412 - ("Accept", "application/json"); 413 - ] 445 + let base_token_headers = 446 + [ 447 + ("Content-Type", "application/x-www-form-urlencoded"); 448 + ("Accept", "application/json"); 449 + ] 414 450 415 - let post_token_endpoint http provider form_str = 451 + let post_token_endpoint http provider ~extra_headers form_str = 416 452 if not (Requests.verify_tls http) then 417 453 invalid_arg 418 454 "Oauth: Requests.t handle must have TLS certificate verification enabled"; 419 455 let url = token_url provider in 420 456 let body = Requests.Body.text form_str in 421 - let resp = Requests.post http url ~body ~headers:token_headers in 457 + let headers = Http.Headers.of_list (base_token_headers @ extra_headers) in 458 + let resp = Requests.post http url ~body ~headers in 422 459 let status = Requests.Response.status_code resp in 423 460 if status < 200 || status >= 300 then begin 424 461 Log.warn (fun m -> m "Token endpoint returned HTTP %d" status); ··· 426 463 end 427 464 else parse_token_response (Requests.Response.text resp) 428 465 429 - let exchange_code http provider ~client_id ~client_secret ~code ~redirect_uri 430 - ?code_verifier () = 431 - let form_str = 432 - exchange_form_body ~client_id ~client_secret ~code ~redirect_uri 433 - ?code_verifier () 466 + let exchange_code http provider ~client_auth ~code ~redirect_uri ?code_verifier 467 + () = 468 + let form_str, extra_headers = 469 + exchange_form_body ~client_auth ~code ~redirect_uri ?code_verifier () 434 470 in 435 - post_token_endpoint http provider form_str 471 + post_token_endpoint http provider ~extra_headers form_str 436 472 437 - let refresh_token http provider ~client_id ~client_secret ~refresh_token = 438 - let form_str = refresh_form_body ~client_id ~client_secret ~refresh_token in 439 - post_token_endpoint http provider form_str 473 + let refresh_token http provider ~client_auth ~refresh_token = 474 + let form_str, extra_headers = refresh_form_body ~client_auth ~refresh_token in 475 + post_token_endpoint http provider ~extra_headers form_str 440 476 441 477 (* -- Token Lifecycle ----------------------------------------------- *) 442 478 ··· 456 492 type t = { 457 493 http : Requests.t; 458 494 provider : provider; 459 - client_id : string; 460 - client_secret : string; 495 + client_auth : Client_auth.t; 461 496 clock : float Eio.Time.clock_ty Eio.Resource.t; 462 497 mutex : Eio.Mutex.t; 463 498 mutable state : state; 464 499 } 465 500 466 - let make http provider ~client_id ~client_secret ~clock ~access_token 467 - ?refresh_token ?expires_at () = 501 + let make http provider ~client_auth ~clock ~access_token ?refresh_token 502 + ?expires_at () = 468 503 { 469 504 http; 470 505 provider; 471 - client_id; 472 - client_secret; 506 + client_auth; 473 507 clock :> float Eio.Time.clock_ty Eio.Resource.t; 474 508 mutex = Eio.Mutex.create (); 475 509 state = { access_token; refresh_token; expires_at }; 476 510 } 477 511 478 - let of_response http provider ~client_id ~client_secret ~clock 479 - (tr : token_response) = 512 + let of_response http provider ~client_auth ~clock (tr : token_response) = 480 513 let now = Eio.Time.now clock in 481 514 let expires_at = 482 515 Option.map (fun d -> now +. float_of_int d) tr.expires_in 483 516 in 484 - make http provider ~client_id ~client_secret ~clock 485 - ~access_token:tr.access_token ?refresh_token:tr.refresh_token ?expires_at 486 - () 517 + make http provider ~client_auth ~clock ~access_token:tr.access_token 518 + ?refresh_token:tr.refresh_token ?expires_at () 487 519 488 520 let stale_state ~clock ~threshold s = 489 521 match s.expires_at with ··· 498 530 Error (Http_error 401) 499 531 | Some rt -> ( 500 532 match 501 - refresh_token t.http t.provider ~client_id:t.client_id 502 - ~client_secret:t.client_secret ~refresh_token:rt 533 + refresh_token t.http t.provider ~client_auth:t.client_auth 534 + ~refresh_token:rt 503 535 with 504 536 | Error _ as e -> e 505 537 | Ok (tr : token_response) -> ··· 590 622 }) 591 623 |> Object.mem "id" int ~enc:(fun _ -> 0) 592 624 |> Object.mem "login" string ~dec_absent:"" ~enc:login 593 - |> Object.mem "email" string ~dec_absent:"" 594 - ~enc:(fun u -> opt_to_string u.email) 625 + |> Object.mem "email" string ~dec_absent:"" ~enc:(fun u -> 626 + opt_to_string u.email) 595 627 |> Object.mem "name" string ~dec_absent:"" ~enc:name 596 628 |> Object.mem "avatar_url" string ~dec_absent:"" ~enc:avatar_url 597 629 |> Object.skip_unknown |> Object.finish ··· 613 645 avatar_url = picture; 614 646 }) 615 647 |> Object.mem "sub" string ~enc:uid 616 - |> Object.mem "email" string ~dec_absent:"" 617 - ~enc:(fun u -> opt_to_string u.email) 648 + |> Object.mem "email" string ~dec_absent:"" ~enc:(fun u -> 649 + opt_to_string u.email) 618 650 |> Object.opt_mem "email_verified" bool ~enc:(fun u -> 619 - Some (email_verified u)) 651 + Some (email_verified u)) 620 652 |> Object.mem "name" string ~dec_absent:"" ~enc:name 621 653 |> Object.mem "picture" string ~dec_absent:"" ~enc:avatar_url 622 654 |> Object.skip_unknown |> Object.finish ··· 639 671 }) 640 672 |> Object.mem "id" int ~enc:(fun _ -> 0) 641 673 |> Object.mem "username" string ~dec_absent:"" ~enc:login 642 - |> Object.mem "email" string ~dec_absent:"" 643 - ~enc:(fun u -> opt_to_string u.email) 674 + |> Object.mem "email" string ~dec_absent:"" ~enc:(fun u -> 675 + opt_to_string u.email) 644 676 |> Object.opt_mem "confirmed_at" string ~enc:(fun u -> 645 - if email_verified u then Some "" else None) 677 + if email_verified u then Some "" else None) 646 678 |> Object.mem "name" string ~dec_absent:"" ~enc:name 647 679 |> Object.mem "avatar_url" string ~dec_absent:"" ~enc:avatar_url 648 680 |> Object.skip_unknown |> Object.finish ··· 664 696 avatar_url = ""; 665 697 }) 666 698 |> Object.mem uid_field string ~enc:uid 667 - |> Object.mem "email" string ~dec_absent:"" 668 - ~enc:(fun u -> opt_to_string u.email) 669 - |> Object.opt_mem "email_verified" bool 670 - ~enc:(fun u -> Some (email_verified u)) 699 + |> Object.mem "email" string ~dec_absent:"" ~enc:(fun u -> 700 + opt_to_string u.email) 701 + |> Object.opt_mem "email_verified" bool ~enc:(fun u -> 702 + Some (email_verified u)) 671 703 |> Object.mem "name" string ~dec_absent:"" ~enc:name 672 704 |> Object.skip_unknown |> Object.finish 673 705
+73 -24
lib/oauth.mli
··· 30 30 let callback_state = (* [state] query param from callback URL *) in 31 31 if not (Oauth.validate_state ~expected:state ~actual:callback_state) then 32 32 failwith "CSRF state mismatch"; 33 + let client_auth = 34 + Oauth.Client_auth.basic ~client_id:"xxx" ~client_secret:"yyy" 35 + in 33 36 match 34 - Oauth.exchange_code http Github ~client_id:"xxx" ~client_secret:"yyy" 37 + Oauth.exchange_code http Github ~client_auth 35 38 ~code ~redirect_uri ~code_verifier:verifier () 36 39 with 37 40 | Ok token -> (* use [token.access_token] *) ··· 228 231 [code_challenge_method] query parameters are included per RFC 7636 §4.3. 229 232 [code_challenge_method] defaults to [S256]. *) 230 233 234 + (** {1:client_auth Client Authentication} 235 + 236 + How a confidential client authenticates to the token endpoint. Per 237 + {{:https://datatracker.ietf.org/doc/html/rfc6749#section-2.3} RFC 6749 §2.3}, 238 + servers MAY accept any of several methods; which one a given deployment 239 + requires is provider-specific. This type abstracts the choice so the same 240 + code works for any provider. 241 + 242 + Choose carefully: 243 + 244 + - [basic] 245 + ({{:https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1} RFC 6749 246 + §2.3.1}) is the preferred method for confidential clients with a shared 247 + secret. The credentials go in the [Authorization] header, which means they 248 + never hit intermediate proxies as a form parameter. 249 + - [post] (same section, alt form) places the secret in the request body. 250 + Still TLS-protected but more likely to leak into logs; use only when the 251 + provider mandates it (GitHub historically required this). 252 + - [none] is for public clients (installed apps, SPAs) that cannot keep a 253 + secret. 254 + 255 + Future variants (not yet implemented): [private_key_jwt] (RFC 7523 §2.2 256 + asymmetric), [client_secret_jwt] (RFC 7523 §2.2 symmetric HMAC). *) 257 + 258 + module Client_auth : sig 259 + type t 260 + 261 + val none : client_id:string -> t 262 + (** Public client. Sends [client_id] in the request body; no credential. *) 263 + 264 + val basic : client_id:string -> client_secret:string -> t 265 + (** HTTP Basic authentication per RFC 6749 §2.3.1. Emits an 266 + [Authorization: Basic base64(client_id:client_secret)] header. Both fields 267 + are percent-encoded before joining, per RFC 6749 §2.3.1. *) 268 + 269 + val post : client_id:string -> client_secret:string -> t 270 + (** Client credentials in the request body per RFC 6749 §2.3.1. Emits 271 + [client_id] and [client_secret] as form fields. *) 272 + 273 + val client_id : t -> string 274 + (** The [client_id] bound to this configuration. *) 275 + 276 + val apply : t -> (string * string) list * (string * string) list 277 + (** [apply t] produces [(form_fields, headers)] to add to a token-endpoint 278 + request. Callers append grant-type-specific fields (e.g. [code], 279 + [refresh_token]) to [form_fields] before form-encoding. For {!none} and 280 + {!post} [headers] is empty; for {!basic} it carries 281 + [Authorization: Basic base64(pct(client_id):pct(client_secret))] per RFC 282 + 6749 §2.3.1. *) 283 + end 284 + 231 285 (** {1:exchange Token Exchange} *) 232 286 233 287 type token_response = { ··· 250 304 val exchange_code : 251 305 Requests.t -> 252 306 provider -> 253 - client_id:string -> 254 - client_secret:string -> 307 + client_auth:Client_auth.t -> 255 308 code:string -> 256 309 redirect_uri:redirect_uri -> 257 310 ?code_verifier:code_verifier -> 258 311 unit -> 259 312 (token_response, parse_token_error) result 260 - (** [exchange_code http provider ~client_id ~client_secret ~code ~redirect_uri 261 - ?code_verifier ()] exchanges an authorization code for an access token by 262 - POSTing to the provider's token endpoint (RFC 6749 §4.1.3). 313 + (** [exchange_code http provider ~client_auth ~code ~redirect_uri ?code_verifier 314 + ()] exchanges an authorization code for an access token by POSTing to the 315 + provider's token endpoint (RFC 6749 §4.1.3). 263 316 317 + [client_auth] controls how the client authenticates; see {!Client_auth}. 264 318 When [~code_verifier] is provided, it is included per RFC 7636 §4.5. 265 319 266 320 @raise Invalid_argument ··· 270 324 val refresh_token : 271 325 Requests.t -> 272 326 provider -> 273 - client_id:string -> 274 - client_secret:string -> 327 + client_auth:Client_auth.t -> 275 328 refresh_token:string -> 276 329 (token_response, parse_token_error) result 277 - (** [refresh_token http provider ~client_id ~client_secret ~refresh_token] 278 - refreshes an access token by POSTing to the provider's token endpoint (RFC 279 - 6749 §6). 330 + (** [refresh_token http provider ~client_auth ~refresh_token] refreshes an 331 + access token by POSTing to the provider's token endpoint (RFC 6749 §6). 280 332 281 333 @raise Invalid_argument 282 334 if TLS verification is disabled (same as {!exchange_code}). *) ··· 295 347 296 348 {b Example} 297 349 {[ 350 + let client_auth = Oauth.Client_auth.basic ~client_id ~client_secret in 298 351 let token = 299 - Oauth.exchange_code http Google ~client_id ~client_secret ~code 300 - ~redirect_uri () 352 + Oauth.exchange_code http Google ~client_auth ~code ~redirect_uri () 301 353 |> Result.get_ok 302 - |> Oauth.Token.of_response http Google ~client_id ~client_secret ~clock 354 + |> Oauth.Token.of_response http Google ~client_auth ~clock 303 355 in 304 356 (* Make many API calls; [access] refreshes if the access token is within 305 357 60 seconds of expiry. *) ··· 313 365 val make : 314 366 Requests.t -> 315 367 provider -> 316 - client_id:string -> 317 - client_secret:string -> 368 + client_auth:Client_auth.t -> 318 369 clock:_ Eio.Time.clock -> 319 370 access_token:string -> 320 371 ?refresh_token:string -> 321 372 ?expires_at:float -> 322 373 unit -> 323 374 t 324 - (** [make http provider ~client_id ~client_secret ~clock ~access_token 325 - ?refresh_token ?expires_at ()] wraps an existing access token. 375 + (** [make http provider ~client_auth ~clock ~access_token ?refresh_token 376 + ?expires_at ()] wraps an existing access token. 326 377 327 378 - [expires_at] is an absolute Unix timestamp. If omitted, the token is 328 379 assumed to have no known expiry and will never auto-refresh. ··· 332 383 val of_response : 333 384 Requests.t -> 334 385 provider -> 335 - client_id:string -> 336 - client_secret:string -> 386 + client_auth:Client_auth.t -> 337 387 clock:_ Eio.Time.clock -> 338 388 token_response -> 339 389 t 340 - (** [of_response http provider ~client_id ~client_secret ~clock tr] wraps a 341 - token response returned by {!exchange_code} or {!refresh_token}. The 342 - expiry is computed from [tr.expires_in] relative to the clock's current 343 - time. *) 390 + (** [of_response http provider ~client_auth ~clock tr] wraps a token response 391 + returned by {!exchange_code} or {!refresh_token}. The expiry is computed 392 + from [tr.expires_in] relative to the clock's current time. *) 344 393 345 394 val access : t -> string 346 395 (** [access t] returns a valid access token, refreshing synchronously if the
+83 -1
test/test_regressions.ml
··· 178 178 "opam should declare crypto-rng.unix when tests require it" true 179 179 (contains opam ~substring:"\"crypto-rng.unix\"") 180 180 181 + (* ── Client authentication (RFC 6749 §2.3.1) ─────────────────────── *) 182 + 183 + let test_client_auth_none_apply () = 184 + let a = Oauth.Client_auth.none ~client_id:"cid" in 185 + let fields, headers = Oauth.Client_auth.apply a in 186 + Alcotest.(check (list (pair string string))) 187 + "fields" 188 + [ ("client_id", "cid") ] 189 + fields; 190 + Alcotest.(check (list (pair string string))) "no headers" [] headers 191 + 192 + let test_client_auth_post_apply () = 193 + let a = 194 + Oauth.Client_auth.post ~client_id:"cid" ~client_secret:"supersecret" 195 + in 196 + let fields, headers = Oauth.Client_auth.apply a in 197 + Alcotest.(check (list (pair string string))) 198 + "fields" 199 + [ ("client_id", "cid"); ("client_secret", "supersecret") ] 200 + fields; 201 + Alcotest.(check (list (pair string string))) "no headers" [] headers 202 + 203 + let test_client_auth_basic_apply () = 204 + let a = Oauth.Client_auth.basic ~client_id:"cid" ~client_secret:"csec" in 205 + let fields, headers = Oauth.Client_auth.apply a in 206 + Alcotest.(check (list (pair string string))) 207 + "fields" 208 + [ ("client_id", "cid") ] 209 + fields; 210 + (* Basic base64("cid:csec") = Basic Y2lkOmNzZWM= *) 211 + Alcotest.(check (list (pair string string))) 212 + "Authorization header" 213 + [ ("Authorization", "Basic Y2lkOmNzZWM=") ] 214 + headers 215 + 216 + let test_client_auth_basic_percent_encodes_special_chars () = 217 + (* RFC 6749 §2.3.1: credentials are form-urlencoded before joining with ":" 218 + so a secret containing ':' or other special chars does not produce an 219 + ambiguous token. We percent-encode both halves uniformly. *) 220 + let a = 221 + Oauth.Client_auth.basic ~client_id:"id:with:colons" 222 + ~client_secret:"p@ss:wor d" 223 + in 224 + let _, headers = Oauth.Client_auth.apply a in 225 + let auth = List.assoc "Authorization" headers in 226 + (* Decode the base64 part and check it's pct(id):pct(secret). *) 227 + let b64 = 228 + match String.split_on_char ' ' auth with 229 + | [ "Basic"; b64 ] -> b64 230 + | _ -> Alcotest.failf "malformed Authorization header: %s" auth 231 + in 232 + let decoded = Base64.decode_exn b64 in 233 + (* client_id and client_secret percent-encoded separately, joined by raw ':' *) 234 + Alcotest.(check string) 235 + "percent-encoded halves joined by ':'" "id%3Awith%3Acolons:p%40ss%3Awor%20d" 236 + decoded 237 + 238 + let test_client_auth_client_id_accessor () = 239 + Alcotest.(check string) 240 + "none" "a" 241 + (Oauth.Client_auth.client_id (Oauth.Client_auth.none ~client_id:"a")); 242 + Alcotest.(check string) 243 + "post" "b" 244 + (Oauth.Client_auth.client_id 245 + (Oauth.Client_auth.post ~client_id:"b" ~client_secret:"x")); 246 + Alcotest.(check string) 247 + "basic" "c" 248 + (Oauth.Client_auth.client_id 249 + (Oauth.Client_auth.basic ~client_id:"c" ~client_secret:"x")) 250 + 181 251 (* ── Transport security ──────────────────────────────────────────── *) 182 252 183 253 let test_custom_provider_rejects_http_token_url () = ··· 313 383 let raised = ref false in 314 384 (try 315 385 ignore 316 - (Oauth.exchange_code http Oauth.Github ~client_id:"x" ~client_secret:"y" 386 + (Oauth.exchange_code http Oauth.Github 387 + ~client_auth: 388 + (Oauth.Client_auth.post ~client_id:"x" ~client_secret:"y") 317 389 ~code:"z" 318 390 ~redirect_uri:(redir "https://example.com/cb") 319 391 ()) ··· 382 454 test_redirect_uri_rejects_fragment; 383 455 Alcotest.test_case "redirect_uri rejects no scheme" `Quick 384 456 test_redirect_uri_rejects_no_scheme; 457 + Alcotest.test_case "Client_auth.none apply" `Quick 458 + test_client_auth_none_apply; 459 + Alcotest.test_case "Client_auth.post apply" `Quick 460 + test_client_auth_post_apply; 461 + Alcotest.test_case "Client_auth.basic apply" `Quick 462 + test_client_auth_basic_apply; 463 + Alcotest.test_case "Client_auth.basic percent-encodes special chars" 464 + `Quick test_client_auth_basic_percent_encodes_special_chars; 465 + Alcotest.test_case "Client_auth.client_id accessor" `Quick 466 + test_client_auth_client_id_accessor; 385 467 Alcotest.test_case "exchange_code rejects verify_tls:false" `Quick 386 468 test_exchange_code_rejects_verify_tls_false; 387 469 Alcotest.test_case "verify_tls getter" `Quick test_verify_tls_getter;
+5 -4
test/test_token.ml
··· 5 5 let clock = Eio.Stdenv.clock env in 6 6 f ~http ~clock 7 7 8 + let auth = Oauth.Client_auth.post ~client_id:"cid" ~client_secret:"csec" 9 + 8 10 let token ~http ~clock ?refresh_token ?expires_at ~access_token () = 9 - Oauth.Token.make http Oauth.Google ~client_id:"cid" ~client_secret:"csec" 10 - ~clock ~access_token ?refresh_token ?expires_at () 11 + Oauth.Token.make http Oauth.Google ~client_auth:auth ~clock ~access_token 12 + ?refresh_token ?expires_at () 11 13 12 14 let test_access_token_roundtrip () = 13 15 with_env @@ fun ~http ~clock -> ··· 72 74 } 73 75 in 74 76 let t = 75 - Oauth.Token.of_response http Oauth.Google ~client_id:"cid" 76 - ~client_secret:"csec" ~clock tr 77 + Oauth.Token.of_response http Oauth.Google ~client_auth:auth ~clock tr 77 78 in 78 79 let expires = Option.get (Oauth.Token.expires_at t) in 79 80 let now = Eio.Time.now clock in