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