OAuth 2.0 authorization and token exchange
0
fork

Configure Feed

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

Fix merlint lint warnings: missing docs, doc style, test inclusion

Resolve E400 (missing documentation), E410 (bad doc style), E615
(missing test suite), and E616 (use failf) across the monorepo.
Also fix test_timing to reference Requests.Timing instead of
non-existent Http.Timing.

+110 -11
+108 -9
test/test_github_oauth.ml
··· 27 27 let url = 28 28 Oauth.authorization_url Oauth.Github ~client_id:"test_client" 29 29 ~redirect_uri:"https://example.com/callback" ~state:"test_state" 30 - ~scope:[ "repo" ] 30 + ~scope:[ "repo" ] () 31 31 in 32 32 Alcotest.(check bool) 33 33 "contains github.com" true ··· 46 46 let url = 47 47 Oauth.authorization_url Oauth.Github ~client_id:"test_client" 48 48 ~redirect_uri:"https://example.com/callback" ~state:"test_state" ~scope:[] 49 + () 49 50 in 50 51 Alcotest.(check bool) 51 52 "no scope param" true ··· 56 57 Oauth.authorization_url Oauth.Github ~client_id:"test_client" 57 58 ~redirect_uri:"https://example.com/callback" ~state:"test_state" 58 59 ~scope:[ "repo"; "user"; "read:org" ] 60 + () 59 61 in 60 62 Alcotest.(check bool) 61 63 "contains scope" true ··· 65 67 let url = 66 68 Oauth.authorization_url Oauth.Google ~client_id:"test_client" 67 69 ~redirect_uri:"https://example.com/callback" ~state:"test_state" 68 - ~scope:[ "openid"; "email" ] 70 + ~scope:[ "openid"; "email" ] () 69 71 in 70 72 Alcotest.(check bool) 71 73 "contains google.com" true ··· 75 77 let body = 76 78 Oauth.exchange_form_body ~client_id:"test_client" 77 79 ~client_secret:"test_secret" ~code:"auth_code" 78 - ~redirect_uri:"https://example.com/callback" 80 + ~redirect_uri:"https://example.com/callback" () 79 81 in 80 82 Alcotest.(check bool) 81 83 "contains client_id" true ··· 97 99 Alcotest.(check string) "access_token" "gho_abc123" t.access_token; 98 100 Alcotest.(check (option int)) "no expires_in" None t.expires_in; 99 101 Alcotest.(check (option string)) "no refresh_token" None t.refresh_token 100 - | Error e -> 101 - Alcotest.fail (Fmt.str "parse failed: %a" Oauth.pp_parse_token_error e) 102 + | Error e -> Alcotest.failf "parse failed: %a" Oauth.pp_parse_token_error e 102 103 103 104 let test_parse_token_github_app () = 104 105 let json = ··· 112 113 "refresh_token" (Some "ghr_xyz789") t.refresh_token; 113 114 Alcotest.(check (option int)) 114 115 "refresh_token_expires_in" (Some 15897600) t.refresh_token_expires_in 115 - | Error e -> 116 - Alcotest.fail (Fmt.str "parse failed: %a" Oauth.pp_parse_token_error e) 116 + | Error e -> Alcotest.failf "parse failed: %a" Oauth.pp_parse_token_error e 117 117 118 118 let test_parse_token_extra_fields () = 119 119 let json = ··· 121 121 in 122 122 match Oauth.parse_token_response json with 123 123 | Ok t -> Alcotest.(check string) "access_token" "gho_test" t.access_token 124 - | Error e -> 125 - Alcotest.fail (Fmt.str "parse failed: %a" Oauth.pp_parse_token_error e) 124 + | Error e -> Alcotest.failf "parse failed: %a" Oauth.pp_parse_token_error e 126 125 127 126 let test_parse_token_invalid_json () = 128 127 let json = "not json" in ··· 146 145 "contains refresh_token" true 147 146 (is_substring body ~substring:"refresh_token=") 148 147 148 + (* ── PKCE (RFC 7636) ─────────────────────────────────────────────── *) 149 + 150 + let test_code_verifier_length () = 151 + let v = Oauth.generate_code_verifier () in 152 + (* 32 bytes → 43 base64url chars *) 153 + Alcotest.(check int) "verifier length is 43" 43 (String.length v) 154 + 155 + let test_code_verifier_charset () = 156 + let v = Oauth.generate_code_verifier () in 157 + String.iter 158 + (fun c -> 159 + let ok = 160 + (c >= 'A' && c <= 'Z') 161 + || (c >= 'a' && c <= 'z') 162 + || (c >= '0' && c <= '9') 163 + || c = '-' || c = '_' || c = '.' || c = '~' 164 + in 165 + Alcotest.(check bool) "unreserved char" true ok) 166 + v 167 + 168 + let test_code_verifier_unique () = 169 + let v1 = Oauth.generate_code_verifier () in 170 + let v2 = Oauth.generate_code_verifier () in 171 + Alcotest.(check bool) "verifiers differ" true (v1 <> v2) 172 + 173 + let test_code_challenge_s256_rfc_vector () = 174 + (* RFC 7636 Appendix B test vector *) 175 + let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" in 176 + let challenge = Oauth.code_challenge S256 verifier in 177 + Alcotest.(check string) 178 + "RFC 7636 Appendix B" "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" 179 + challenge 180 + 181 + let test_code_challenge_plain () = 182 + let verifier = "some_verifier_string" in 183 + let challenge = Oauth.code_challenge Plain verifier in 184 + Alcotest.(check string) "plain = verifier" verifier challenge 185 + 186 + let test_authorization_url_with_pkce () = 187 + let verifier = Oauth.generate_code_verifier () in 188 + let challenge = Oauth.code_challenge S256 verifier in 189 + let url = 190 + Oauth.authorization_url Oauth.Github ~client_id:"test_client" 191 + ~redirect_uri:"https://example.com/callback" ~state:"test_state" 192 + ~scope:[ "repo" ] ~code_challenge:challenge () 193 + in 194 + Alcotest.(check bool) 195 + "contains code_challenge" true 196 + (is_substring url ~substring:"code_challenge="); 197 + Alcotest.(check bool) 198 + "contains code_challenge_method=S256" true 199 + (is_substring url ~substring:"code_challenge_method=S256") 200 + 201 + let test_authorization_url_pkce_plain () = 202 + let challenge = "my_plain_challenge" in 203 + let url = 204 + Oauth.authorization_url Oauth.Github ~client_id:"test_client" 205 + ~redirect_uri:"https://example.com/callback" ~state:"test_state" ~scope:[] 206 + ~code_challenge:challenge ~code_challenge_method:Plain () 207 + in 208 + Alcotest.(check bool) 209 + "contains code_challenge_method=plain" true 210 + (is_substring url ~substring:"code_challenge_method=plain") 211 + 212 + let test_exchange_with_code_verifier () = 213 + let body = 214 + Oauth.exchange_form_body ~client_id:"test_client" 215 + ~client_secret:"test_secret" ~code:"auth_code" 216 + ~redirect_uri:"https://example.com/callback" 217 + ~code_verifier:"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" () 218 + in 219 + Alcotest.(check bool) 220 + "contains code_verifier" true 221 + (is_substring body ~substring:"code_verifier=") 222 + 223 + let test_exchange_without_code_verifier () = 224 + let body = 225 + Oauth.exchange_form_body ~client_id:"test_client" 226 + ~client_secret:"test_secret" ~code:"auth_code" 227 + ~redirect_uri:"https://example.com/callback" () 228 + in 229 + Alcotest.(check bool) 230 + "no code_verifier" true 231 + (not (is_substring body ~substring:"code_verifier=")) 232 + 149 233 let suite = 150 234 ( "oauth", 151 235 [ ··· 172 256 Alcotest.test_case "parse_token invalid json" `Quick 173 257 test_parse_token_invalid_json; 174 258 Alcotest.test_case "refresh request body" `Quick test_refresh_request_body; 259 + Alcotest.test_case "PKCE verifier length" `Quick test_code_verifier_length; 260 + Alcotest.test_case "PKCE verifier charset" `Quick 261 + test_code_verifier_charset; 262 + Alcotest.test_case "PKCE verifier unique" `Quick test_code_verifier_unique; 263 + Alcotest.test_case "PKCE S256 RFC vector" `Quick 264 + test_code_challenge_s256_rfc_vector; 265 + Alcotest.test_case "PKCE plain challenge" `Quick test_code_challenge_plain; 266 + Alcotest.test_case "PKCE authorization_url" `Quick 267 + test_authorization_url_with_pkce; 268 + Alcotest.test_case "PKCE authorization_url plain" `Quick 269 + test_authorization_url_pkce_plain; 270 + Alcotest.test_case "PKCE exchange with verifier" `Quick 271 + test_exchange_with_code_verifier; 272 + Alcotest.test_case "PKCE exchange without verifier" `Quick 273 + test_exchange_without_code_verifier; 175 274 ] )
+2 -2
test/test_regressions.ml
··· 24 24 let url = 25 25 Oauth.authorization_url Oauth.Github ~client_id:"test_client" 26 26 ~redirect_uri:"https://example.com/callback" ~state:"test_state" 27 - ~scope:[ "repo" ] 27 + ~scope:[ "repo" ] () 28 28 in 29 29 let uri = Uri.of_string url in 30 30 Alcotest.(check (option string)) ··· 35 35 let body = 36 36 Oauth.exchange_form_body ~client_id:"test_client" 37 37 ~client_secret:"test_secret" ~code:"auth_code" 38 - ~redirect_uri:"https://example.com/callback" 38 + ~redirect_uri:"https://example.com/callback" () 39 39 in 40 40 let query = Uri.query_of_encoded body in 41 41 Alcotest.(check (option (list string)))