ghtool/github/
auth_client.rs1use eyre::Result;
2use http::HeaderMap;
3use serde::Deserialize;
4use tracing::{error, info};
5
6pub struct GithubAuthClient {
7 client: reqwest::Client,
8}
9
10const GITHUB_BASE_URI: &str = "https://github.com";
11const CLIENT_ID: &str = "32a2525cc736ee9b63ae";
12const USER_AGENT: &str = "ghtool";
13const GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code";
14
15#[derive(Deserialize, Debug)]
16pub struct CodeResponse {
17 pub device_code: String,
18 pub user_code: String,
19 pub verification_uri: String,
20 pub expires_in: u32,
21 pub interval: u32,
22}
23
24#[derive(Deserialize, Debug)]
25pub struct AccessToken {
26 pub access_token: String,
27 pub scope: String,
28 pub token_type: String,
29}
30
31#[derive(Deserialize, Debug)]
32pub struct Error {
33 pub error: String,
34 pub error_description: String,
35 pub error_uri: String,
36}
37
38pub enum AccessTokenResponse {
39 AuthorizationPending(Error),
40 AccessToken(AccessToken),
41}
42
43impl GithubAuthClient {
44 pub fn new() -> Result<Self> {
45 let client = reqwest::Client::builder()
46 .user_agent(USER_AGENT)
47 .default_headers(make_headers())
48 .build()
49 .map_err(|e| eyre::eyre!("Failed to build client: {}", e))?;
50
51 Ok(Self { client })
52 }
53
54 pub async fn get_device_code(&self) -> Result<CodeResponse> {
55 let params = [("client_id", CLIENT_ID), ("scope", "repo")];
56 let url = format!("{}/login/device/code", GITHUB_BASE_URI);
57 info!("Requesting device code from {}", url);
58 let res = self.client.post(url).form(¶ms).send().await?;
59 let code_response: CodeResponse = res.json().await?;
60 info!("Received device code: {:?}", code_response);
61 Ok(code_response)
62 }
63
64 pub async fn get_access_token(&self, device_code: &str) -> Result<AccessTokenResponse> {
65 let params = [
66 ("client_id", CLIENT_ID),
67 ("device_code", device_code),
68 ("grant_type", GRANT_TYPE),
69 ];
70 let url = format!("{}/login/oauth/access_token", GITHUB_BASE_URI);
71 info!("Requesting access token from {}", url);
72 let res = self.client.post(url).form(¶ms).send().await?;
73
74 if res.status().is_success() {
75 let bytes = res.bytes().await?;
76 let token_response: Result<AccessToken, _> = serde_json::from_slice(&bytes);
77 info!("Received response: {:?}", token_response);
78 match token_response {
79 Ok(token) => Ok(AccessTokenResponse::AccessToken(token)),
80 Err(_) => {
81 let error_response: Error = serde_json::from_slice(&bytes)?;
82 if error_response.error == "authorization_pending" {
83 info!(?error_response, "Authorization pending");
84 Ok(AccessTokenResponse::AuthorizationPending(error_response))
85 } else {
86 error!(?error_response, "Unexpected error");
87 Err(eyre::eyre!(
88 "Unexpected error: {} - {}",
89 error_response.error,
90 error_response.error_description
91 ))
92 }
93 }
94 }
95 } else {
96 Err(eyre::eyre!("Failed to get access token"))
97 }
98 }
99}
100
101fn make_headers() -> HeaderMap {
102 let mut headers = reqwest::header::HeaderMap::new();
103 headers.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap());
104 headers
105}