alibi_api/plugins/oauth/providers/
github.rs1use super::*;
2impl OAuthProvider {
3 #[must_use]
4 pub fn github(client_id: &str, client_secret: &str) -> Self {
5 Self::github_with_endpoints(
6 client_id,
7 client_secret,
8 "https://github.com/login/oauth/authorize",
9 "https://github.com/login/oauth/access_token",
10 "https://api.github.com/user",
11 "https://api.github.com/user/emails",
12 )
13 }
14
15 #[must_use]
20 pub fn github_with_endpoints(
21 client_id: &str,
22 client_secret: &str,
23 auth_url: &str,
24 token_url: &str,
25 user_info_url: &str,
26 user_emails_url: &str,
27 ) -> Self {
28 Self {
29 client_id: client_id.to_owned(),
30 additional_client_ids: Vec::new(),
31 hosted_domain: None,
32 require_email_verification: false,
33 client_secret: client_secret.to_owned(),
34 auth_url: auth_url.to_owned(),
35 token_url: token_url.to_owned(),
36 user_info_url: Some(user_info_url.to_owned()),
37 scopes: vec!["read:user".to_owned(), "user:email".to_owned()],
38 authorization: Some(OAuthAuthorizationPolicy::default()),
39 authorization_params: Vec::new(),
40 account_subject: None,
41 map_user_info: None,
42 get_user_info: Some(Arc::new(GitHubUserInfoHandler::new(
43 user_info_url.to_owned(),
44 user_emails_url.to_owned(),
45 ))),
46 refresh_access_token: None,
47 verify_id_token: None,
48 id_token: None,
49 disable_id_token_sign_in: false,
50 disable_implicit_sign_up: false,
51 disable_sign_up: false,
52 override_user_info_on_sign_in: false,
53 }
54 }
55}
56
57#[derive(Debug, Deserialize)]
58pub(in crate::plugins::oauth::providers) struct GitHubEmailAddress {
59 pub(in crate::plugins::oauth::providers) email: String,
60 #[serde(default)]
61 pub(in crate::plugins::oauth::providers) primary: bool,
62 #[serde(default)]
63 pub(in crate::plugins::oauth::providers) verified: bool,
64}
65
66#[derive(Clone)]
67pub(in crate::plugins::oauth::providers) struct GitHubUserInfoHandler {
68 pub(in crate::plugins::oauth::providers) user_url: String,
69 pub(in crate::plugins::oauth::providers) emails_url: String,
70}
71
72impl GitHubUserInfoHandler {
73 pub(in crate::plugins::oauth::providers) const fn new(
74 user_url: String,
75 emails_url: String,
76 ) -> Self {
77 Self {
78 user_url,
79 emails_url,
80 }
81 }
82
83 pub(in crate::plugins::oauth::providers) async fn fetch_json<T: DeserializeOwned>(
84 &self,
85 client: &reqwest::Client,
86 url: &str,
87 access_token: &str,
88 ) -> Result<T, String> {
89 let response = client
90 .get(url)
91 .bearer_auth(access_token)
92 .header("Accept", "application/json")
93 .header("User-Agent", "better-auth")
94 .send()
95 .await
96 .map_err(|error| format!("Failed to fetch GitHub user info: {error}"))?;
97
98 if !response.status().is_success() {
99 let body = response
100 .text()
101 .await
102 .unwrap_or_else(|_| "Unknown error".to_owned());
103 return Err(format!("GitHub user info request failed: {body}"));
104 }
105
106 response
107 .json()
108 .await
109 .map_err(|error| format!("Failed to parse GitHub user info: {error}"))
110 }
111}
112
113#[async_trait]
114impl OAuthUserInfoHandler for GitHubUserInfoHandler {
115 async fn get_user_info(
116 &self,
117 request: OAuthUserInfoRequest,
118 ) -> Result<OAuthUserInfoResponse, String> {
119 let access_token = request
120 .access_token
121 .as_deref()
122 .ok_or("Missing access token for user-info lookup")?;
123
124 let client = reqwest::Client::new();
125 let mut profile: Value = self
126 .fetch_json(&client, &self.user_url, access_token)
127 .await?;
128 let emails = self
129 .fetch_json::<Vec<GitHubEmailAddress>>(&client, &self.emails_url, access_token)
130 .await
131 .unwrap_or_default();
132
133 let resolved_email = profile
134 .get("email")
135 .and_then(Value::as_str)
136 .map(String::from)
137 .or_else(|| {
138 emails
139 .iter()
140 .find(|record| record.primary)
141 .or_else(|| emails.first())
142 .map(|record| record.email.clone())
143 })
144 .unwrap_or_default();
145
146 if let Some(profile_object) = profile.as_object_mut()
147 && profile_object
148 .get("email")
149 .and_then(Value::as_str)
150 .is_none()
151 && !resolved_email.is_empty()
152 {
153 drop(profile_object.insert("email".to_owned(), Value::String(resolved_email.clone())));
154 }
155
156 let email_verified = emails
157 .iter()
158 .find(|record| record.email == resolved_email)
159 .is_some_and(|record| record.verified);
160
161 let id = profile
162 .get("id")
163 .and_then(|value| value.as_i64().map(|value| value.to_string()))
164 .or_else(|| profile.get("id").and_then(Value::as_str).map(String::from))
165 .ok_or("missing id")?;
166
167 let login = profile
168 .get("login")
169 .and_then(Value::as_str)
170 .map(String::from);
171
172 Ok(OAuthUserInfoResponse {
173 user_output: None,
174 user: OAuthUserInfo {
175 additional_fields: Default::default(),
176 id,
177 email: resolved_email,
178 name: profile
179 .get("name")
180 .and_then(Value::as_str)
181 .map(String::from)
182 .or(login),
183 image: profile
184 .get("avatar_url")
185 .and_then(Value::as_str)
186 .map(String::from),
187 email_verified,
188 },
189 data: profile,
190 })
191 }
192}