oauth2-facebook 0.1.3

OAuth 2.0 Facebook
Documentation
use std::error;

use oauth2_client::{
    device_authorization_grant::provider_ext::{
        AccessTokenResponseErrorBody, AccessTokenResponseSuccessfulBody,
        BodyWithDeviceAuthorizationGrant, DeviceAccessTokenEndpointRetryReason,
        DeviceAuthorizationResponseErrorBody, DeviceAuthorizationResponseSuccessfulBody,
    },
    oauth2_core::{
        device_authorization_grant::device_authorization_response::{
            DeviceCode, UserCode, VerificationUri, VerificationUriComplete,
        },
        re_exports::AccessTokenResponseErrorBodyError,
    },
    re_exports::{
        serde_json, thiserror, Body, ClientId, ClientSecret, Deserialize, Map, Response,
        SerdeJsonError, Serialize, Url, UrlParseError, Value,
    },
    Provider, ProviderExtDeviceAuthorizationGrant,
};

use crate::{FacebookScope, DEVICE_AUTHORIZATION_URL, DEVICE_TOKEN_URL};

#[derive(Debug, Clone)]
pub struct FacebookProviderForDevices {
    client_access_token: String,
    pub redirect_uri: Option<String>,
    //
    token_endpoint_url: Url,
    device_authorization_endpoint_url: Url,
}
impl FacebookProviderForDevices {
    pub fn new(app_id: String, client_token: String) -> Result<Self, UrlParseError> {
        Ok(Self {
            client_access_token: format!("{}|{}", app_id, client_token),
            redirect_uri: None,
            token_endpoint_url: DEVICE_TOKEN_URL.parse()?,
            device_authorization_endpoint_url: DEVICE_AUTHORIZATION_URL.parse()?,
        })
    }

    pub fn configure<F>(mut self, mut f: F) -> Self
    where
        F: FnMut(&mut Self),
    {
        f(&mut self);
        self
    }
}
impl Provider for FacebookProviderForDevices {
    type Scope = FacebookScope;

    fn client_id(&self) -> Option<&ClientId> {
        None
    }

    fn client_secret(&self) -> Option<&ClientSecret> {
        None
    }

    fn token_endpoint_url(&self) -> &Url {
        &self.token_endpoint_url
    }
}
impl ProviderExtDeviceAuthorizationGrant for FacebookProviderForDevices {
    fn device_authorization_endpoint_url(&self) -> &Url {
        &self.device_authorization_endpoint_url
    }

    fn device_authorization_request_body_extra(&self) -> Option<Map<String, Value>> {
        let mut map = Map::new();

        map.insert(
            "access_token".to_owned(),
            Value::String(self.client_access_token.to_owned()),
        );

        if let Some(redirect_uri) = &self.redirect_uri {
            map.insert(
                "redirect_uri".to_owned(),
                Value::String(redirect_uri.to_owned()),
            );
        }

        Some(map)
    }

    fn device_authorization_response_parsing(
        &self,
        response: &Response<Body>,
    ) -> Option<
        Result<
            Result<DeviceAuthorizationResponseSuccessfulBody, DeviceAuthorizationResponseErrorBody>,
            Box<dyn error::Error + Send + Sync + 'static>,
        >,
    > {
        fn doing(
            response: &Response<Body>,
        ) -> Result<
            Result<
                FacebookDeviceAuthorizationResponseSuccessfulBody,
                FacebookDeviceAuthorizationResponseErrorBody,
            >,
            Box<dyn error::Error + Send + Sync + 'static>,
        > {
            if response.status().is_success() {
                let map = serde_json::from_slice::<Map<String, Value>>(response.body())
                    .map_err(DeviceAuthorizationResponseParsingError::DeResponseBodyFailed)?;
                if !map.contains_key("error") {
                    let body = serde_json::from_slice::<
                        FacebookDeviceAuthorizationResponseSuccessfulBody,
                    >(response.body())
                    .map_err(DeviceAuthorizationResponseParsingError::DeResponseBodyFailed)?;

                    return Ok(Ok(body));
                }
            }

            let body = serde_json::from_slice::<FacebookDeviceAuthorizationResponseErrorBody>(
                response.body(),
            )
            .map_err(DeviceAuthorizationResponseParsingError::DeResponseBodyFailed)?;
            Ok(Err(body))
        }

        match doing(response) {
            Ok(Ok(ok_body)) => Some(Ok(Ok(ok_body.into()))),
            Ok(Err(err_body)) => match DeviceAuthorizationResponseErrorBody::try_from(err_body) {
                Ok(err_body) => Some(Ok(Err(err_body))),
                Err(err) => Some(Err(err)),
            },
            Err(err) => Some(Err(err)),
        }
    }

    fn device_access_token_request_body_extra(
        &self,
        body: &BodyWithDeviceAuthorizationGrant,
        _device_authorization_response_body: &DeviceAuthorizationResponseSuccessfulBody,
    ) -> Option<Map<String, Value>> {
        let mut map = Map::new();

        map.insert(
            "access_token".to_owned(),
            Value::String(self.client_access_token.to_owned()),
        );

        map.insert(
            "code".to_owned(),
            Value::String(body.device_code.to_owned()),
        );

        Some(map)
    }

    #[allow(clippy::type_complexity)]
    fn device_access_token_response_parsing(
        &self,
        response: &Response<Body>,
    ) -> Option<
        Result<
            Result<
                Result<
                    AccessTokenResponseSuccessfulBody<<Self as Provider>::Scope>,
                    AccessTokenResponseErrorBody,
                >,
                DeviceAccessTokenEndpointRetryReason,
            >,
            Box<dyn error::Error + Send + Sync + 'static>,
        >,
    > {
        fn doing(
            response: &Response<Body>,
        ) -> Result<
            Result<
                AccessTokenResponseSuccessfulBody<<FacebookProviderForDevices as Provider>::Scope>,
                FacebookDeviceAccessTokenResponseErrorBody,
            >,
            Box<dyn error::Error + Send + Sync + 'static>,
        > {
            if response.status().is_success() {
                let map = serde_json::from_slice::<Map<String, Value>>(response.body())
                    .map_err(DeviceAccessTokenResponseParsingError::DeResponseBodyFailed)?;
                if !map.contains_key("error") {
                    let body = serde_json::from_slice::<
                        AccessTokenResponseSuccessfulBody<
                            <FacebookProviderForDevices as Provider>::Scope,
                        >,
                    >(response.body())
                    .map_err(DeviceAccessTokenResponseParsingError::DeResponseBodyFailed)?;

                    return Ok(Ok(body));
                }
            }

            let body = serde_json::from_slice::<FacebookDeviceAccessTokenResponseErrorBody>(
                response.body(),
            )
            .map_err(DeviceAuthorizationResponseParsingError::DeResponseBodyFailed)?;
            Ok(Err(body))
        }

        match doing(response) {
            Ok(Ok(ok_body)) => Some(Ok(Ok(Ok(ok_body)))),
            Ok(Err(err_body)) => match Result::<
                DeviceAccessTokenEndpointRetryReason,
                AccessTokenResponseErrorBody,
            >::try_from(err_body)
            {
                Ok(Ok(reason)) => Some(Ok(Err(reason))),
                Ok(Err(err_body)) => Some(Ok(Ok(Err(err_body)))),
                Err(err) => Some(Err(err)),
            },
            Err(err) => Some(Err(err)),
        }
    }
}

//
#[derive(Serialize, Deserialize)]
pub struct FacebookDeviceAuthorizationResponseSuccessfulBody {
    pub code: DeviceCode,
    pub user_code: UserCode,
    pub verification_uri: VerificationUri,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub verification_uri_complete: Option<VerificationUriComplete>,
    pub expires_in: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub interval: Option<usize>,
}
impl From<FacebookDeviceAuthorizationResponseSuccessfulBody>
    for DeviceAuthorizationResponseSuccessfulBody
{
    fn from(body: FacebookDeviceAuthorizationResponseSuccessfulBody) -> Self {
        Self::new(
            body.code.to_owned(),
            body.user_code.to_owned(),
            body.verification_uri.to_owned(),
            body.verification_uri_complete.to_owned(),
            body.expires_in.to_owned(),
            body.interval.to_owned(),
        )
    }
}

#[derive(Serialize, Deserialize)]
pub struct FacebookDeviceAuthorizationResponseErrorBody {
    pub error: FacebookDeviceAuthorizationResponseErrorBodyError,
}
#[derive(Serialize, Deserialize)]
pub struct FacebookDeviceAuthorizationResponseErrorBodyError {
    pub message: String,
}
impl TryFrom<FacebookDeviceAuthorizationResponseErrorBody>
    for DeviceAuthorizationResponseErrorBody
{
    type Error = Box<dyn error::Error + Send + Sync>;

    fn try_from(body: FacebookDeviceAuthorizationResponseErrorBody) -> Result<Self, Self::Error> {
        let mut body_new = Self::new(
            AccessTokenResponseErrorBodyError::Other("".to_owned()),
            Some(body.error.message.to_owned()),
            None,
        );
        body_new.set_extra(
            serde_json::to_value(body)
                .map(|x| x.as_object().cloned())?
                .ok_or_else(|| "unreachable".to_owned())?,
        );

        Ok(body_new)
    }
}

#[derive(thiserror::Error, Debug)]
pub enum DeviceAuthorizationResponseParsingError {
    //
    #[error("DeResponseBodyFailed {0}")]
    DeResponseBodyFailed(SerdeJsonError),
}

//
#[derive(Serialize, Deserialize)]
pub struct FacebookDeviceAccessTokenResponseErrorBody {
    pub error: FacebookDeviceAccessTokenResponseErrorBodyError,
}
#[derive(Serialize, Deserialize)]
pub struct FacebookDeviceAccessTokenResponseErrorBodyError {
    pub message: String,
    pub error_subcode: Option<isize>,
}
impl TryFrom<FacebookDeviceAccessTokenResponseErrorBody>
    for Result<DeviceAccessTokenEndpointRetryReason, AccessTokenResponseErrorBody>
{
    type Error = Box<dyn error::Error + Send + Sync>;

    fn try_from(body: FacebookDeviceAccessTokenResponseErrorBody) -> Result<Self, Self::Error> {
        match body.error.error_subcode {
            Some(1349174) => Ok(Ok(
                DeviceAccessTokenEndpointRetryReason::AuthorizationPending,
            )),
            Some(1349172) => Ok(Ok(DeviceAccessTokenEndpointRetryReason::SlowDown)),
            Some(1349152) => {
                let mut body_new = AccessTokenResponseErrorBody::new(
                    AccessTokenResponseErrorBodyError::ExpiredToken,
                    Some(body.error.message.to_owned()),
                    None,
                );
                body_new.set_extra(
                    serde_json::to_value(body)
                        .map(|x| x.as_object().cloned())?
                        .ok_or_else(|| "unreachable".to_owned())?,
                );

                Ok(Err(body_new))
            }
            _ => {
                let mut body_new = AccessTokenResponseErrorBody::new(
                    AccessTokenResponseErrorBodyError::Other("".to_owned()),
                    Some(body.error.message.to_owned()),
                    None,
                );
                body_new.set_extra(
                    serde_json::to_value(body)
                        .map(|x| x.as_object().cloned())?
                        .ok_or_else(|| "unreachable".to_owned())?,
                );

                Ok(Err(body_new))
            }
        }
    }
}

#[derive(thiserror::Error, Debug)]
pub enum DeviceAccessTokenResponseParsingError {
    //
    #[error("DeResponseBodyFailed {0}")]
    DeResponseBodyFailed(SerdeJsonError),
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::error;

    use oauth2_client::{
        device_authorization_grant::{DeviceAccessTokenEndpoint, DeviceAuthorizationEndpoint},
        re_exports::{Endpoint as _, RetryableEndpoint as _},
    };

    #[test]
    fn authorization_request() -> Result<(), Box<dyn error::Error>> {
        let provider =
            FacebookProviderForDevices::new("APP_ID".to_owned(), "CLIENT_TOKEN".to_owned())?;
        let endpoint = DeviceAuthorizationEndpoint::new(
            &provider,
            vec![FacebookScope::Email, FacebookScope::PublicProfile],
        );

        //
        let request = endpoint.render_request()?;

        assert_eq!(
            request.body(),
            b"scope=email+public_profile&access_token=APP_ID%7CCLIENT_TOKEN"
        );

        Ok(())
    }

    #[test]
    fn authorization_response() -> Result<(), Box<dyn error::Error>> {
        let provider =
            FacebookProviderForDevices::new("APP_ID".to_owned(), "CLIENT_TOKEN".to_owned())?;
        let endpoint = DeviceAuthorizationEndpoint::new(
            &provider,
            vec![FacebookScope::Email, FacebookScope::PublicProfile],
        );

        //
        let response_body =
            include_str!("../tests/response_body_json_files/device_authorization.json");
        let body_ret = endpoint
            .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
        match body_ret {
            Ok(body) => {
                assert_eq!(body.device_code, "4c7c240847a4c10bf6850802c51dde1e")
            }
            Err(body) => panic!("{:?}", body),
        }

        //
        let response_body = include_str!(
            "../tests/response_body_json_files/device_authorization_err_when_no_access_token.json"
        );
        let body_ret = endpoint
            .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
        match body_ret {
            Ok(body) => panic!("{:?}", body),
            Err(body) => assert_eq!(
                body.error_description,
                Some("(#190) This method must be called with a client access token".to_owned())
            ),
        }

        Ok(())
    }

    #[test]
    fn access_token_request() -> Result<(), Box<dyn error::Error>> {
        let provider =
            FacebookProviderForDevices::new("APP_ID".to_owned(), "CLIENT_TOKEN".to_owned())?;
        let endpoint = DeviceAccessTokenEndpoint::new(
            &provider,
            DeviceAuthorizationResponseSuccessfulBody::new(
                "DEVICE_CODE".to_owned(),
                "".to_owned(),
                "https://example.com".parse()?,
                None,
                0,
                Some(5),
            ),
        );

        //
        let request = endpoint.render_request(None)?;

        assert_eq!(request.body(), b"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=DEVICE_CODE&access_token=APP_ID%7CCLIENT_TOKEN&code=DEVICE_CODE");

        Ok(())
    }

    #[test]
    fn access_token_response() -> Result<(), Box<dyn error::Error>> {
        let provider =
            FacebookProviderForDevices::new("APP_ID".to_owned(), "CLIENT_TOKEN".to_owned())?;
        let endpoint = DeviceAccessTokenEndpoint::new(
            &provider,
            DeviceAuthorizationResponseSuccessfulBody::new(
                "DEVICE_CODE".to_owned(),
                "".to_owned(),
                "https://example.com".parse()?,
                None,
                0,
                Some(5),
            ),
        );

        //
        let response_body = include_str!(
            "../tests/response_body_json_files/access_token_with_device_authorization_grant.json"
        );
        let body_ret = endpoint.parse_response(
            Response::builder().body(response_body.as_bytes().to_vec())?,
            None,
        )?;
        match body_ret {
            Ok(Ok(body)) => {
                let map = body.extra().unwrap();
                assert_eq!(
                    map.get("data_access_expiration_time").unwrap().as_u64(),
                    Some(1644569029)
                );
            }
            Ok(Err(body)) => panic!("{:?}", body),
            Err(reason) => panic!("{:?}", reason),
        }

        //
        let response_body = include_str!(
            "../tests/response_body_json_files/device_access_token_err_when_1349174.json"
        );
        let body_ret = endpoint.parse_response(
            Response::builder().body(response_body.as_bytes().to_vec())?,
            None,
        )?;
        match body_ret {
            Ok(Ok(body)) => panic!("{:?}", body),
            Ok(Err(body)) => panic!("{:?}", body),
            Err(reason) => assert_eq!(
                reason,
                DeviceAccessTokenEndpointRetryReason::AuthorizationPending
            ),
        }

        Ok(())
    }
}