auth_framework/profile_utils/
mod.rs1use crate::errors::{AuthError, OAuthProviderError, Result};
4use crate::providers::{OAuthProvider, OAuthTokenResponse, UserProfile};
5use crate::tokens::AuthToken;
6use reqwest::Client;
7use serde_json::Value;
8#[allow(async_fn_in_trait)]
12pub trait TokenToProfile {
13 async fn to_profile(&self, provider: &OAuthProvider) -> Result<UserProfile>;
15}
16
17pub trait ExtractProfile {
19 fn extract_profile(
21 &self,
22 provider: &OAuthProvider,
23 json_response: Value,
24 ) -> Result<UserProfile>;
25}
26
27impl TokenToProfile for OAuthTokenResponse {
28 async fn to_profile(&self, provider: &OAuthProvider) -> Result<UserProfile> {
29 let config = provider.config();
30 let userinfo_url = config.userinfo_url.clone().ok_or_else(|| {
31 AuthError::OAuthProvider(OAuthProviderError::UnsupportedFeature {
32 provider: format!("{:?}", provider),
33 feature: "userinfo endpoint".to_string(),
34 })
35 })?;
36
37 let client = Client::new();
38 let response = client
39 .get(&userinfo_url)
40 .bearer_auth(&self.access_token)
41 .send()
42 .await
43 .map_err(|e| AuthError::NetworkError(format!("Failed to fetch user profile: {}", e)))?;
44
45 let status = response.status();
46 if !status.is_success() {
47 return Err(AuthError::NetworkError(format!(
48 "Failed to fetch user profile. Status code: {}",
49 status
50 )));
51 }
52
53 let json_response = response.json::<Value>().await.map_err(|e| {
54 AuthError::ParseError(format!("Failed to parse user profile response: {}", e))
55 })?;
56
57 provider.extract_profile(provider, json_response)
58 }
59}
60
61impl ExtractProfile for OAuthProvider {
62 fn extract_profile(
63 &self,
64 _provider: &OAuthProvider,
65 json_response: Value,
66 ) -> Result<UserProfile> {
67 let mut profile = UserProfile::new();
69
70 match self {
72 OAuthProvider::GitHub => {
73 profile = profile.with_id(extract_string(&json_response, "id")?);
74 profile = profile.with_username(extract_string_optional(&json_response, "login"));
75 profile = profile.with_name(extract_string_optional(&json_response, "name"));
76 profile = profile.with_email(extract_string_optional(&json_response, "email"));
77 profile =
78 profile.with_picture(extract_string_optional(&json_response, "avatar_url"));
79 }
80 OAuthProvider::Google => {
81 profile = profile.with_id(extract_string(&json_response, "sub")?);
82 profile = profile.with_email(extract_string_optional(&json_response, "email"));
83 profile = profile.with_name(extract_string_optional(&json_response, "name"));
84 profile = profile.with_picture(extract_string_optional(&json_response, "picture"));
85 profile = profile.with_locale(extract_string_optional(&json_response, "locale"));
86
87 if let Some(email_verified) = json_response
88 .get("email_verified")
89 .and_then(|v| v.as_bool())
90 {
91 profile = profile.with_email_verified(email_verified);
92 }
93 }
94 OAuthProvider::Microsoft => {
95 profile = profile.with_id(extract_string(&json_response, "id")?);
96 profile = profile.with_email(
97 extract_string_optional(&json_response, "userPrincipalName")
98 .or_else(|| extract_string_optional(&json_response, "mail")),
99 );
100 profile = profile.with_name(extract_string_optional(&json_response, "displayName"));
101
102 if let Some(photo) = json_response.get("photo") {
104 profile = profile.with_picture(photo.as_str().map(String::from));
105 }
106 }
107 OAuthProvider::Discord => {
108 profile = profile.with_id(extract_string(&json_response, "id")?);
109 profile =
110 profile.with_username(extract_string_optional(&json_response, "username"));
111 profile = profile.with_email(extract_string_optional(&json_response, "email"));
112
113 if let Some(avatar) = extract_string_optional(&json_response, "avatar") {
114 let id = extract_string_optional(&json_response, "id").unwrap_or_default();
115 let avatar_url =
116 format!("https://cdn.discordapp.com/avatars/{}/{}.png", id, avatar);
117 profile = profile.with_picture(Some(avatar_url));
118 }
119 }
120 OAuthProvider::GitLab => {
121 profile = profile.with_id(extract_string(&json_response, "id")?);
122 profile =
123 profile.with_username(extract_string_optional(&json_response, "username"));
124 profile = profile.with_name(extract_string_optional(&json_response, "name"));
125 profile = profile.with_email(extract_string_optional(&json_response, "email"));
126 profile =
127 profile.with_picture(extract_string_optional(&json_response, "avatar_url"));
128 }
129 OAuthProvider::Twitter => {
130 if let Some(data) = json_response.get("data") {
132 profile = profile.with_id(extract_string_from_value(data, "id")?);
133 profile =
134 profile.with_username(extract_string_optional_from_value(data, "username"));
135 profile = profile.with_name(extract_string_optional_from_value(data, "name"));
136 } else {
137 profile = profile.with_id(extract_string(&json_response, "id")?);
138 profile = profile
139 .with_username(extract_string_optional(&json_response, "screen_name"));
140 profile = profile.with_name(extract_string_optional(&json_response, "name"));
141 profile = profile.with_picture(extract_string_optional(
142 &json_response,
143 "profile_image_url_https",
144 ));
145 }
146 }
147 OAuthProvider::Facebook => {
148 profile = profile.with_id(extract_string(&json_response, "id")?);
149 profile = profile.with_name(extract_string_optional(&json_response, "name"));
150 profile = profile.with_email(extract_string_optional(&json_response, "email"));
151 if let Some(id) = extract_string_optional(&json_response, "id") {
153 let picture_url =
154 format!("https://graph.facebook.com/{}/picture?type=large", id);
155 profile = profile.with_picture(Some(picture_url));
156 }
157 }
158 OAuthProvider::LinkedIn => {
159 profile = profile.with_id(extract_string(&json_response, "id")?);
160 if let Some(name) = json_response
162 .get("localizedFirstName")
163 .and_then(|f| f.as_str())
164 .zip(
165 json_response
166 .get("localizedLastName")
167 .and_then(|l| l.as_str()),
168 )
169 {
170 profile = profile.with_name(Some(format!("{} {}", name.0, name.1)));
171 }
172 }
174 OAuthProvider::Custom { name, .. } => {
175 let id = extract_string_optional(&json_response, "id")
177 .or_else(|| extract_string_optional(&json_response, "sub"))
178 .or_else(|| extract_string_optional(&json_response, "user_id"));
179
180 if let Some(id) = id {
181 profile = profile.with_id(id);
182 } else {
183 return Err(AuthError::validation(format!(
184 "Could not find ID field in response from custom provider {}",
185 name
186 )));
187 }
188
189 profile = profile.with_email(extract_string_optional(&json_response, "email"));
191 profile = profile.with_name(
192 extract_string_optional(&json_response, "name")
193 .or_else(|| extract_string_optional(&json_response, "display_name")),
194 );
195 profile = profile.with_username(
196 extract_string_optional(&json_response, "username")
197 .or_else(|| extract_string_optional(&json_response, "login")),
198 );
199 profile = profile.with_picture(
200 extract_string_optional(&json_response, "picture")
201 .or_else(|| extract_string_optional(&json_response, "avatar"))
202 .or_else(|| extract_string_optional(&json_response, "avatar_url")),
203 );
204 }
205 }
206
207 profile = profile.with_additional_data("raw_profile", json_response);
209 Ok(profile)
210 }
211}
212
213impl TokenToProfile for AuthToken {
214 async fn to_profile(&self, _provider: &OAuthProvider) -> Result<UserProfile> {
215 let mut profile = UserProfile::new();
217
218 profile = profile.with_id(self.user_id.clone());
220
221 profile = profile.with_provider(self.auth_method.clone());
223
224 if let Some(name) = self.metadata.custom.get("name").and_then(|v| v.as_str()) {
226 profile = profile.with_name(Some(name.to_string()));
227 }
228
229 if let Some(email) = self.metadata.custom.get("email").and_then(|v| v.as_str()) {
230 profile = profile.with_email(Some(email.to_string()));
231 }
232
233 Ok(profile)
236 }
237}
238
239fn extract_string(json: &Value, field: &str) -> Result<String> {
241 json.get(field)
242 .and_then(|v| v.as_str())
243 .map(String::from)
244 .ok_or_else(|| {
245 AuthError::validation(format!("Field '{}' not found or not a string", field))
246 })
247}
248
249fn extract_string_optional(json: &Value, field: &str) -> Option<String> {
251 json.get(field).and_then(|v| v.as_str()).map(String::from)
252}
253
254fn extract_string_from_value(json: &Value, field: &str) -> Result<String> {
256 json.get(field)
257 .and_then(|v| v.as_str())
258 .map(String::from)
259 .ok_or_else(|| {
260 AuthError::validation(format!("Field '{}' not found or not a string", field))
261 })
262}
263
264fn extract_string_optional_from_value(json: &Value, field: &str) -> Option<String> {
266 json.get(field).and_then(|v| v.as_str()).map(String::from)
267}
268
269