Skip to main content

better_auth_api/plugins/oauth/providers/
facebook.rs

1use 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/// Facebook profile fields and additional app audiences.
10#[derive(Clone, Default)]
11pub struct FacebookOptions {
12    /// Extra Graph fields appended after the default profile fields.
13    pub fields: Vec<String>,
14    /// Additional accepted audiences; requests still use the primary client ID.
15    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    /// Configure Facebook Graph login and verified Limited Login profiles.
24    /// Set `config_id` through `authorization_params` for Facebook Login for Business.
25    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}