Skip to content

Commit 47c546b

Browse files
committed
add functions to query the Slack API for oauth exchange
1 parent 9bd0cae commit 47c546b

File tree

6 files changed

+69
-0
lines changed

6 files changed

+69
-0
lines changed

lib/api.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ end
1010

1111
module type Slack = sig
1212
val send_notification : ctx:Context.t -> msg:post_message_req -> (unit, string) Result.t Lwt.t
13+
14+
val update_access_token_of_context : ctx:Context.t -> code:string -> (unit, string) Result.t Lwt.t
1315
end

lib/api_local.ml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ module Slack : Api.Slack = struct
2626
Stdio.printf "will notify #%s\n" msg.channel;
2727
Stdio.printf "%s\n" json;
2828
Lwt.return @@ Ok ()
29+
30+
let update_access_token_of_context ~ctx:_ ~code:_ =
31+
Stdio.printf "will generate token\n";
32+
Lwt.return @@ Ok ()
2933
end
3034

3135
module Slack_simple : Api.Slack = struct
@@ -38,6 +42,8 @@ module Slack_simple : Api.Slack = struct
3842
| Some s -> Printf.sprintf " with %S" s
3943
);
4044
Lwt.return @@ Ok ()
45+
46+
let update_access_token_of_context ~ctx:_ ~code:_ = Lwt.return @@ Error "undefined for local setup"
4147
end
4248

4349
module Slack_json : Api.Slack = struct
@@ -51,4 +57,6 @@ module Slack_json : Api.Slack = struct
5157
log#info "%s" (Uri.to_string url);
5258
log#info "%s" json;
5359
Lwt.return @@ Ok ()
60+
61+
let update_access_token_of_context ~ctx:_ ~code:_ = Lwt.return @@ Error "undefined for local setup"
5462
end

lib/api_remote.ml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,36 @@ module Slack : Api.Slack = struct
8080
)
8181
| Error e -> Lwt.return @@ build_query_error url e
8282
)
83+
84+
let access_token_of_code ~(ctx : Context.t) ~code =
85+
let secrets = Context.get_secrets_exn ctx in
86+
let body = `Form [ "code", code ] in
87+
match secrets.slack_client_id with
88+
| None -> Lwt.return @@ Error "slack_client_id is undefined"
89+
| Some client_id ->
90+
match secrets.slack_client_secret with
91+
| None -> Lwt.return @@ Error "slack_client_secret is undefined"
92+
| Some client_secret ->
93+
let auth_header = Base64.encode_string @@ sprintf "%s:%s" client_id client_secret in
94+
let headers = [ Printf.sprintf "Authorization: Basic %s" auth_header ] in
95+
( match%lwt Common.http_request ~body ~headers `POST "https://slack.com/api/oauth.v2.access" with
96+
| Error e -> Lwt.return @@ Error e
97+
| Ok data ->
98+
let response = Slack_j.oauth_access_res_of_string data in
99+
( match response.access_token, response.error with
100+
| Some access_token, _ -> Lwt.return @@ Ok access_token
101+
| None, Some e -> Lwt.return @@ Error e
102+
| None, None -> Lwt.return @@ Error "an unknown error occurred while getting access token"
103+
)
104+
)
105+
106+
let update_access_token_of_context ~ctx ~code =
107+
let secrets = Context.get_secrets_exn ctx in
108+
match%lwt access_token_of_code ~ctx ~code with
109+
| Error e -> Lwt.return @@ Error e
110+
| Ok access_token ->
111+
let secrets = { secrets with slack_access_token = Some access_token } in
112+
ctx.secrets <- Some secrets;
113+
let data = Config_j.string_of_secrets secrets |> Yojson.Basic.from_string |> Yojson.Basic.pretty_to_string in
114+
Lwt.return @@ write_to_local_file ~data ctx.secrets_filepath
83115
end

lib/config.atd

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,13 @@ type secrets = {
4747
~slack_hooks <ocaml default="[]"> : webhook list;
4848
(* Slack bot token obtained via OAuth, enabling message posting to the workspace *)
4949
?slack_access_token : string nullable;
50+
(* Client ID of the Slack bot; used for OAuth authentication *)
51+
?slack_client_id : string nullable;
52+
(* Client secret of the Slack bot; used for OAuth authentication *)
53+
?slack_client_secret : string nullable;
54+
(* Slack uses this secret to sign requests; provide to verify incoming Slack requests *)
55+
?slack_signing_secret : string nullable;
56+
(* if specified, maintains state b/w OAuth request and callback to prevent CSRF
57+
see: https://tools.ietf.org/html/rfc6749#section-4.1.1 *)
58+
?slack_oauth_state : string nullable;
5059
}

lib/slack.atd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,10 @@ type post_message_res = {
6565
?channel: string nullable;
6666
?error: string nullable;
6767
}
68+
69+
(* expected payload when exchanging oauth code for access token *)
70+
type oauth_access_res = {
71+
ok: bool;
72+
?access_token: string option;
73+
?error: string option;
74+
}

lib/slack.ml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,14 @@ let generate_commit_comment_notification api_commit notification channel =
358358
}
359359
in
360360
{ channel; text = None; attachments = Some [ attachment ]; blocks = None }
361+
362+
let validate_state ?oauth_state ~args =
363+
match oauth_state with
364+
| None -> Ok ()
365+
| Some expected_state ->
366+
match List.Assoc.find args "state" ~equal:String.equal with
367+
| None -> Error "argument `state` not found in slack authorization request"
368+
| Some state ->
369+
match String.equal state expected_state with
370+
| false -> Error "argument `state` is present in slack authorization request but does not match expected value"
371+
| true -> Ok ()

0 commit comments

Comments
 (0)