GitHub OAuth helpers
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. *)