better_auth_api/plugins/oauth/providers/
facebook.rs1use better_auth_core::{AuthError, AuthResult, SchemaValue};
2use serde_json::{Value, json};
3
4use super::{
5 OAuthProvider, OAuthUserInfo, OAuthUserInfoRequest, OAuthUserInfoResponse, ProviderKind,
6};
7use crate::plugins::oauth::{oidc::OidcVerifier, social_profile::fetch_http_profile};
8
9#[derive(Clone, Default)]
11pub struct FacebookOptions {
12 pub fields: Vec<String>,
14 pub additional_client_ids: Vec<String>,
16 #[cfg(test)]
17 pub(in crate::plugins::oauth) jwks_url: Option<String>,
18 #[cfg(test)]
19 pub(in crate::plugins::oauth) debug_token_url: Option<String>,
20}
21
22impl OAuthProvider {
23 pub fn facebook(client_id: &str, client_secret: &str, options: FacebookOptions) -> Self {
26 Self {
27 kind: ProviderKind::Facebook(options),
28 user_info_url: Some("https://graph.facebook.com/me".into()),
29 ..Self::custom(
30 client_id,
31 client_secret,
32 "https://www.facebook.com/v24.0/dialog/oauth",
33 "https://graph.facebook.com/v24.0/oauth/access_token",
34 )
35 }
36 }
37
38 pub(in crate::plugins::oauth) fn facebook_options(&self) -> Option<&FacebookOptions> {
39 match &self.kind {
40 ProviderKind::Facebook(options) => Some(options),
41 _ => None,
42 }
43 }
44}
45
46impl FacebookOptions {
47 pub(in crate::plugins::oauth) async fn verify(
48 &self,
49 client_id: &str,
50 token: &str,
51 nonce: Option<&str>,
52 ) -> AuthResult<Value> {
53 let jwks_url = "https://limited.facebook.com/.well-known/oauth/openid/jwks/";
54 #[cfg(test)]
55 let jwks_url = self.jwks_url.as_deref().unwrap_or(jwks_url);
56 let audiences = std::iter::once(client_id.to_owned())
57 .chain(self.additional_client_ids.iter().cloned())
58 .collect();
59 let verifier = OidcVerifier::with_policy(
60 jwks_url
61 .parse()
62 .map_err(|error: url::ParseError| AuthError::internal(error.to_string()))?,
63 "https://www.facebook.com".into(),
64 audiences,
65 Some(vec![json!("RS256")]),
66 None,
67 )
68 .map_err(|error| AuthError::internal(error.to_string()))?;
69 verifier
70 .verify(token, nonce)
71 .await
72 .map_err(|_| crate::plugins::oauth::id_token::invalid())
73 }
74}
75
76pub(in crate::plugins::oauth) async fn fetch_user_info(
77 provider: &OAuthProvider,
78 options: &FacebookOptions,
79 request: OAuthUserInfoRequest,
80 claims: Option<Value>,
81) -> AuthResult<Option<OAuthUserInfoResponse>> {
82 let (data, limited) = if let Some(claims) = claims {
83 (claims, true)
84 } else {
85 let Some(access_token) = request
86 .access_token
87 .as_deref()
88 .filter(|token| !token.is_empty())
89 else {
90 return Ok(None);
91 };
92 if provider.client_id.is_empty() || provider.client_secret.is_empty() {
93 return Ok(None);
94 }
95 let debug_url = "https://graph.facebook.com/debug_token";
96 #[cfg(test)]
97 let debug_url = options.debug_token_url.as_deref().unwrap_or(debug_url);
98 let inspection = reqwest::Client::new().get(debug_url).query(&[
99 ("input_token", access_token),
100 (
101 "access_token",
102 &format!("{}|{}", provider.client_id, provider.client_secret),
103 ),
104 ]);
105 let Some(inspection) = fetch_http_profile(inspection).await? else {
106 return Ok(None);
107 };
108 let Some(data) = inspection.get("data") else {
109 return Ok(None);
110 };
111 let Some(app_id) = data
112 .get("app_id")
113 .and_then(Value::as_str)
114 .filter(|id| !id.is_empty())
115 else {
116 return Ok(None);
117 };
118 let Some(user_id) = data
119 .get("user_id")
120 .and_then(Value::as_str)
121 .filter(|id| !id.is_empty())
122 else {
123 return Ok(None);
124 };
125 if data.get("is_valid") != Some(&Value::Bool(true))
126 || (app_id != provider.client_id
127 && !options.additional_client_ids.iter().any(|id| id == app_id))
128 {
129 return Ok(None);
130 }
131 let endpoint = provider
132 .user_info_url
133 .as_deref()
134 .ok_or_else(|| AuthError::internal("Missing user_info_url for provider"))?;
135 let fields = ["id", "name", "email", "picture"]
136 .into_iter()
137 .chain(options.fields.iter().map(String::as_str))
138 .collect::<Vec<_>>()
139 .join(",");
140 let profile = reqwest::Client::new()
141 .get(endpoint)
142 .query(&[("fields", fields)])
143 .bearer_auth(access_token);
144 let Some(profile) = fetch_http_profile(profile).await? else {
145 return Ok(None);
146 };
147 if profile.get("id").and_then(Value::as_str) != Some(user_id) {
148 return Ok(None);
149 }
150 (profile, false)
151 };
152 let user = if let Some(decode) = provider.map_user_info {
153 decode(data.clone())
154 } else {
155 decode_profile(&data, limited)
156 }
157 .map_err(AuthError::internal)?;
158 let mut response = OAuthUserInfoResponse { user, data };
159 if let Some(mapper) = &provider.map_profile_to_user {
160 let mapped = mapper.map_profile(&response.data).await?;
161 crate::plugins::oauth::social_profile::apply_mapped_profile(&mut response, mapped);
162 }
163 let _ = response.user.email()?;
164 let _ = response.user.email_verified()?;
165 Ok(Some(response))
166}
167
168pub(super) fn graph_profile(profile: Value) -> Result<OAuthUserInfo, String> {
169 decode_profile(&profile, false)
170}
171
172fn decode_profile(profile: &Value, limited: bool) -> Result<OAuthUserInfo, String> {
173 Ok(OAuthUserInfo {
174 id: profile
175 .get(if limited { "sub" } else { "id" })
176 .and_then(Value::as_str)
177 .ok_or("missing Facebook subject")?
178 .into(),
179 name: profile
180 .get("name")
181 .cloned()
182 .map(serde_json::from_value::<Option<String>>)
183 .transpose()
184 .map(|value| value.map(Into::into).unwrap_or_default())
185 .map_err(|error| format!("Invalid Facebook name: {error}"))?,
186 email: super::defaults::profile_email(profile)?,
187 image: (if limited {
188 profile.get("picture")
189 } else {
190 profile.pointer("/picture/data/url")
191 })
192 .cloned()
193 .map(serde_json::from_value)
194 .transpose()
195 .map_err(|error| format!("Invalid Facebook picture: {error}"))?,
196 email_verified: if limited {
197 Some(false).into()
198 } else {
199 SchemaValue::from_json(Some(
200 profile
201 .get("email_verified")
202 .filter(|value| !value.is_null())
203 .cloned()
204 .unwrap_or(Value::Bool(false)),
205 ))
206 .map_err(|error| error.to_string())?
207 },
208 additional_fields: Default::default(),
209 })
210}