OAuth 2.0 authorization and token exchange
0
fork

Configure Feed

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

Add UNIQUE constraint enforcement to ocaml-sqlite, fix OAuth identity race

ocaml-sqlite: Parse and enforce UNIQUE constraints (column-level and
table-level) via in-memory hash indexes. Raises Unique_violation on
conflict. Indexes are rebuilt on database reopen.

ocaml-auth: Add UNIQUE(provider, provider_uid) to oauth_identities
schema. Callback now catches Unique_violation on concurrent creates
and falls back to the existing identity.

ocaml-oauth: Classify parse_token_response errors into Invalid_json,
Missing_access_token, and Invalid_token_format instead of collapsing
all failures to Invalid_json.

+63 -2
+22 -2
lib/oauth.ml
··· 131 131 | Missing_access_token -> Fmt.pf fmt "Missing access_token field" 132 132 | Invalid_token_format -> Fmt.pf fmt "Invalid token format" 133 133 134 + let has_substring ~sub s = 135 + let len = String.length sub in 136 + let rec loop i = 137 + if i + len > String.length s then false 138 + else if String.sub s i len = sub then true 139 + else loop (i + 1) 140 + in 141 + loop 0 142 + 143 + let classify_token_error body e = 144 + match decode Jsont.json body with 145 + | Error _ -> Invalid_json 146 + | Ok _ -> 147 + if 148 + has_substring ~sub:"Missing member" e 149 + && has_substring ~sub:"access_token" e 150 + then Missing_access_token 151 + else Invalid_token_format 152 + 134 153 let parse_token_response body = 135 154 match decode token_response_jsont body with 136 155 | Ok t -> Ok t 137 156 | Error e -> 138 - Log.warn (fun m -> m "Token parse failed: %s" e); 139 - Error Invalid_json 157 + let err = classify_token_error body e in 158 + Log.warn (fun m -> m "Token parse failed: %a" pp_parse_token_error err); 159 + Error err 140 160 141 161 (* ── Token Refresh ───────────────────────────────────────────────── *) 142 162
+41
test/test_regressions.ml
··· 72 72 "refresh_token preserved" (Some [ "ghr_abc123" ]) 73 73 (query_param "refresh_token" query) 74 74 75 + let parse_token_error = Alcotest.testable Oauth.pp_parse_token_error ( = ) 76 + 77 + let test_parse_token_response_ok () = 78 + let body = 79 + {|{"access_token":"gho_abc","expires_in":3600,"refresh_token":"ghr_xyz"}|} 80 + in 81 + match Oauth.parse_token_response body with 82 + | Ok t -> 83 + Alcotest.(check string) "access_token" "gho_abc" t.access_token; 84 + Alcotest.(check (option int)) "expires_in" (Some 3600) t.expires_in; 85 + Alcotest.(check (option string)) 86 + "refresh_token" (Some "ghr_xyz") t.refresh_token 87 + | Error e -> 88 + Alcotest.failf "expected Ok, got Error %a" Oauth.pp_parse_token_error e 89 + 90 + let test_parse_token_response_invalid_json () = 91 + let body = "not json at all" in 92 + Alcotest.(check (result reject parse_token_error)) 93 + "invalid json" (Error Oauth.Invalid_json) 94 + (Oauth.parse_token_response body) 95 + 96 + let test_parse_token_response_missing_access_token () = 97 + let body = {|{"expires_in":3600}|} in 98 + Alcotest.(check (result reject parse_token_error)) 99 + "missing access_token" (Error Oauth.Missing_access_token) 100 + (Oauth.parse_token_response body) 101 + 102 + let test_parse_token_response_invalid_format () = 103 + let body = {|{"access_token":12345}|} in 104 + Alcotest.(check (result reject parse_token_error)) 105 + "access_token wrong type" (Error Oauth.Invalid_token_format) 106 + (Oauth.parse_token_response body) 107 + 75 108 let test_readme_uses_current_api_names () = 76 109 let readme = read_file [ "README.md"; "ocaml-oauth/README.md" ] in 77 110 Alcotest.(check bool) ··· 109 142 test_exchange_request_body_uses_form_encoding; 110 143 Alcotest.test_case "refresh_request_body uses form encoding" `Quick 111 144 test_refresh_request_body_uses_form_encoding; 145 + Alcotest.test_case "parse_token_response ok" `Quick 146 + test_parse_token_response_ok; 147 + Alcotest.test_case "parse_token_response invalid json" `Quick 148 + test_parse_token_response_invalid_json; 149 + Alcotest.test_case "parse_token_response missing access_token" `Quick 150 + test_parse_token_response_missing_access_token; 151 + Alcotest.test_case "parse_token_response invalid format" `Quick 152 + test_parse_token_response_invalid_format; 112 153 ] )