GitHub OAuth helpers
at main 112 lines 4.0 kB view raw
1(** GitHub OAuth URL generation and token exchange helpers. 2 3 Supports both GitHub Apps (with token expiry and refresh tokens) and 4 traditional OAuth Apps. 5 6 {2 Standards} 7 8 - {{:https://datatracker.ietf.org/doc/html/rfc6749} RFC 6749} - OAuth 2.0 9 - {{:https://datatracker.ietf.org/doc/html/rfc7636} RFC 7636} - PKCE 10 11 {2 Example} 12 13 {[ 14 (* Generate authorization URL *) 15 let state = Github_oauth.generate_state () in 16 let url = 17 Github_oauth.authorization_url ~client_id:"your_client_id" 18 ~callback_url:"https://yourapp.com/callback" ~state ~scope:[ "repo" ] 19 in 20 21 (* After user authorizes, exchange code for token *) 22 let body = 23 Github_oauth.exchange_request_body ~client_id:"your_client_id" 24 ~client_secret:"your_secret" ~code ~redirect_uri:"https://yourapp.com/callback" 25 in 26 (* POST body to Github_oauth.access_token_url with Accept: application/json *) 27 ]} *) 28 29(** {1 State Generation} *) 30 31val generate_state : unit -> string 32(** [generate_state ()] generates a cryptographically secure random state for 33 CSRF protection. Returns a 64-character lowercase hex string (32 random 34 bytes). 35 36 @raise Crypto_rng.Unseeded_generator if RNG not initialized. *) 37 38(** {1 Authorization URL} *) 39 40val authorization_url : 41 client_id:string -> 42 callback_url:string -> 43 state:string -> 44 scope:string list -> 45 string 46(** [authorization_url ~client_id ~callback_url ~state ~scope] generates a 47 GitHub OAuth authorization URL. 48 49 @param client_id Your GitHub App or OAuth App client ID 50 @param callback_url URL GitHub will redirect to after authorization 51 @param state Random state for CSRF protection (from {!generate_state}) 52 @param scope 53 List of requested scopes. Use empty list for GitHub Apps (permissions 54 configured in app settings). Common scopes: ["repo"], ["user"], 55 ["read:org"]. *) 56 57(** {1 Token Exchange} *) 58 59val access_token_url : string 60(** [access_token_url] is the GitHub access token exchange endpoint: 61 [https://github.com/login/oauth/access_token]. *) 62 63val exchange_request_body : 64 client_id:string -> 65 client_secret:string -> 66 code:string -> 67 redirect_uri:string -> 68 string 69(** [exchange_request_body ~client_id ~client_secret ~code ~redirect_uri] 70 generates a JSON request body for exchanging an authorization code for an 71 access token. 72 73 POST this to {!access_token_url} with headers: 74 - [Content-Type: application/json] 75 - [Accept: application/json]. *) 76 77(** {1 Token Response} *) 78 79type token_response = { 80 access_token : string; (** The access token *) 81 expires_in : int option; 82 (** Seconds until expiry. [Some 28800] for GitHub Apps, [None] for OAuth 83 Apps *) 84 refresh_token : string option; 85 (** Refresh token. [Some _] for GitHub Apps, [None] for OAuth Apps *) 86 refresh_token_expires_in : int option; 87 (** Seconds until refresh token expiry (~6 months for GitHub Apps) *) 88} 89(** Token response structure supporting both GitHub Apps and OAuth Apps. *) 90 91type parse_token_error = 92 | Invalid_json (** JSON parsing failed *) 93 | Missing_access_token (** Required access_token field missing *) 94 | Invalid_token_format (** Token fields have unexpected format *) 95 96val parse_token_response : string -> (token_response, parse_token_error) result 97(** [parse_token_response body] parses a GitHub token response JSON. 98 99 Supports both GitHub App responses (with expiry and refresh tokens) and 100 OAuth App responses (access token only). *) 101 102val pp_parse_token_error : Format.formatter -> parse_token_error -> unit 103(** Pretty-printer for parse errors. *) 104 105(** {1 Token Refresh} *) 106 107val refresh_request_body : 108 client_id:string -> client_secret:string -> refresh_token:string -> string 109(** [refresh_request_body ~client_id ~client_secret ~refresh_token] generates a 110 JSON request body for refreshing a GitHub App access token. 111 112 POST this to {!access_token_url} with the same headers as token exchange. *)