(** GitHub OAuth URL generation and token exchange helpers. *) let src = Logs.Src.create "github-oauth" ~doc:"GitHub OAuth helpers" module Log = (val Logs.src_log src : Logs.LOG) (* JSON helpers *) let decode codec s = Jsont_bytesrw.decode_string codec s let encode codec v = Jsont_bytesrw.encode_string codec v |> Result.value ~default:"{}" let generate_state () = Ohex.encode (Crypto_rng.generate 32) let authorization_url ~client_id ~callback_url ~state ~scope = let base_query = [ ("client_id", [ client_id ]); ("redirect_uri", [ callback_url ]); ("state", [ state ]); ] in let query = match scope with | [] -> base_query | lst -> ("scope", [ String.concat " " lst ]) :: base_query in let uri = Uri.make ~scheme:"https" ~host:"github.com" ~path:"/login/oauth/authorize" ~query () in Uri.to_string uri let access_token_url = "https://github.com/login/oauth/access_token" (* JSON codecs *) let exchange_body_jsont = Jsont.Object.map ~kind:"exchange_body" (fun client_id client_secret code redirect_uri -> (client_id, client_secret, code, redirect_uri)) |> Jsont.Object.mem "client_id" Jsont.string ~enc:(fun (c, _, _, _) -> c) |> Jsont.Object.mem "client_secret" Jsont.string ~enc:(fun (_, s, _, _) -> s) |> Jsont.Object.mem "code" Jsont.string ~enc:(fun (_, _, c, _) -> c) |> Jsont.Object.mem "redirect_uri" Jsont.string ~enc:(fun (_, _, _, r) -> r) |> Jsont.Object.finish let exchange_request_body ~client_id ~client_secret ~code ~redirect_uri = encode exchange_body_jsont (client_id, client_secret, code, redirect_uri) type token_response = { access_token : string; expires_in : int option; refresh_token : string option; refresh_token_expires_in : int option; } let token_response_jsont = Jsont.Object.map ~kind:"token_response" (fun access_token expires_in refresh_token refresh_token_expires_in -> { access_token; expires_in; refresh_token; refresh_token_expires_in }) |> Jsont.Object.mem "access_token" Jsont.string ~enc:(fun t -> t.access_token) |> Jsont.Object.opt_mem "expires_in" Jsont.int ~enc:(fun t -> t.expires_in) |> Jsont.Object.opt_mem "refresh_token" Jsont.string ~enc:(fun t -> t.refresh_token) |> Jsont.Object.opt_mem "refresh_token_expires_in" Jsont.int ~enc:(fun t -> t.refresh_token_expires_in) |> Jsont.Object.skip_unknown |> Jsont.Object.finish type parse_token_error = | Invalid_json | Missing_access_token | Invalid_token_format let pp_parse_token_error fmt = function | Invalid_json -> Fmt.pf fmt "Invalid JSON" | Missing_access_token -> Fmt.pf fmt "Missing access_token field" | Invalid_token_format -> Fmt.pf fmt "Invalid token format" let parse_token_response body = match decode token_response_jsont body with | Ok t -> Ok t | Error e -> Log.warn (fun m -> m "Token parse failed: %s" e); Error Invalid_json let refresh_body_jsont = Jsont.Object.map ~kind:"refresh_body" (fun client_id client_secret grant_type refresh_token -> (client_id, client_secret, grant_type, refresh_token)) |> Jsont.Object.mem "client_id" Jsont.string ~enc:(fun (c, _, _, _) -> c) |> Jsont.Object.mem "client_secret" Jsont.string ~enc:(fun (_, s, _, _) -> s) |> Jsont.Object.mem "grant_type" Jsont.string ~enc:(fun (_, _, g, _) -> g) |> Jsont.Object.mem "refresh_token" Jsont.string ~enc:(fun (_, _, _, r) -> r) |> Jsont.Object.finish let refresh_request_body ~client_id ~client_secret ~refresh_token = encode refresh_body_jsont (client_id, client_secret, "refresh_token", refresh_token)