From 0baf8d199783da596cf2277fe4950e6290da7dc9 Mon Sep 17 00:00:00 2001 From: Sebastian Jeltsch Date: Mon, 18 Nov 2024 22:41:27 +0100 Subject: [PATCH] Add untested Facebook and Microsoft oauth providers. --- proto/config.proto | 2 + .../src/auth/oauth/providers/discord.rs | 2 +- .../src/auth/oauth/providers/facebook.rs | 122 ++++++++++++++++++ .../src/auth/oauth/providers/gitlab.rs | 16 +-- .../src/auth/oauth/providers/google.rs | 4 +- .../src/auth/oauth/providers/microsoft.rs | 109 ++++++++++++++++ .../src/auth/oauth/providers/mod.rs | 4 + 7 files changed, 247 insertions(+), 12 deletions(-) create mode 100644 trailbase-core/src/auth/oauth/providers/facebook.rs create mode 100644 trailbase-core/src/auth/oauth/providers/microsoft.rs diff --git a/proto/config.proto b/proto/config.proto index fe898db6..9342887e 100644 --- a/proto/config.proto +++ b/proto/config.proto @@ -31,6 +31,8 @@ enum OAuthProviderId { DISCORD = 10; GITLAB = 11; GOOGLE = 12; + FACEBOOK = 13; + MICROSOFT = 14; } message OAuthProviderConfig { diff --git a/trailbase-core/src/auth/oauth/providers/discord.rs b/trailbase-core/src/auth/oauth/providers/discord.rs index cde89de7..93bd413a 100644 --- a/trailbase-core/src/auth/oauth/providers/discord.rs +++ b/trailbase-core/src/auth/oauth/providers/discord.rs @@ -31,7 +31,7 @@ impl DiscordOAuthProvider { )); }; - return Ok(DiscordOAuthProvider { + return Ok(Self { client_id, client_secret, }); diff --git a/trailbase-core/src/auth/oauth/providers/facebook.rs b/trailbase-core/src/auth/oauth/providers/facebook.rs new file mode 100644 index 00000000..68d95ffb --- /dev/null +++ b/trailbase-core/src/auth/oauth/providers/facebook.rs @@ -0,0 +1,122 @@ +use async_trait::async_trait; +use lazy_static::lazy_static; +use serde::Deserialize; +use url::Url; + +use crate::auth::oauth::providers::{OAuthProviderError, OAuthProviderFactory}; +use crate::auth::oauth::{OAuthClientSettings, OAuthProvider, OAuthUser}; +use crate::auth::AuthError; +use crate::config::proto::{OAuthProviderConfig, OAuthProviderId}; + +#[derive(Default, Deserialize, Debug)] +struct FacebookUserPictureData { + url: String, +} + +#[derive(Default, Deserialize, Debug)] +struct FacebookUserPicture { + data: FacebookUserPictureData, +} + +#[derive(Default, Deserialize, Debug)] +struct FacebookUser { + id: String, + email: String, + // name: Option, + picture: Option, +} + +pub(crate) struct FacebookOAuthProvider { + client_id: String, + client_secret: String, +} + +impl FacebookOAuthProvider { + const NAME: &'static str = "facebook"; + const DISPLAY_NAME: &'static str = "Facebook"; + + const AUTH_URL: &'static str = "https://www.facebook.com/v3.2/dialog/oauth"; + const TOKEN_URL: &'static str = "https://graph.facebook.com/v3.2/oauth/access_token"; + const USER_API_URL: &'static str = + "https://graph.facebook.com/me?fields=name,email,picture.type(large)"; + + fn new(config: &OAuthProviderConfig) -> Result { + let Some(client_id) = config.client_id.clone() else { + return Err(OAuthProviderError::Missing( + "Facebook client id".to_string(), + )); + }; + let Some(client_secret) = config.client_secret.clone() else { + return Err(OAuthProviderError::Missing( + "Facebook client secret".to_string(), + )); + }; + + return Ok(Self { + client_id, + client_secret, + }); + } + + pub fn factory() -> OAuthProviderFactory { + OAuthProviderFactory { + id: OAuthProviderId::Facebook, + name: Self::NAME, + display_name: Self::DISPLAY_NAME, + factory: Box::new(|config: &OAuthProviderConfig| Ok(Box::new(Self::new(config)?))), + } + } +} + +#[async_trait] +impl OAuthProvider for FacebookOAuthProvider { + fn name(&self) -> &'static str { + Self::NAME + } + fn provider(&self) -> OAuthProviderId { + OAuthProviderId::Facebook + } + fn display_name(&self) -> &'static str { + Self::DISPLAY_NAME + } + + fn settings(&self) -> Result { + lazy_static! { + static ref AUTH_URL: Url = Url::parse(FacebookOAuthProvider::AUTH_URL).unwrap(); + static ref TOKEN_URL: Url = Url::parse(FacebookOAuthProvider::TOKEN_URL).unwrap(); + } + + return Ok(OAuthClientSettings { + auth_url: AUTH_URL.clone(), + token_url: TOKEN_URL.clone(), + client_id: self.client_id.clone(), + client_secret: self.client_secret.clone(), + }); + } + + fn oauth_scopes(&self) -> Vec<&'static str> { + return vec!["email"]; + } + + async fn get_user(&self, access_token: String) -> Result { + let response = reqwest::Client::new() + .get(Self::USER_API_URL) + .bearer_auth(access_token) + .send() + .await + .map_err(|err| AuthError::FailedDependency(err.into()))?; + + let user = response + .json::() + .await + .map_err(|err| AuthError::FailedDependency(err.into()))?; + + return Ok(OAuthUser { + provider_user_id: user.id, + provider_id: OAuthProviderId::Facebook, + email: user.email, + verified: true, + avatar: user.picture.map(|p| p.data.url), + }); + } +} diff --git a/trailbase-core/src/auth/oauth/providers/gitlab.rs b/trailbase-core/src/auth/oauth/providers/gitlab.rs index e6f42dc5..3fd846cf 100644 --- a/trailbase-core/src/auth/oauth/providers/gitlab.rs +++ b/trailbase-core/src/auth/oauth/providers/gitlab.rs @@ -31,7 +31,7 @@ impl GitlabOAuthProvider { )); }; - return Ok(GitlabOAuthProvider { + return Ok(Self { client_id, client_secret, }); @@ -40,11 +40,9 @@ impl GitlabOAuthProvider { pub fn factory() -> OAuthProviderFactory { OAuthProviderFactory { id: OAuthProviderId::Gitlab, - name: GitlabOAuthProvider::NAME, - display_name: GitlabOAuthProvider::DISPLAY_NAME, - factory: Box::new(|config: &OAuthProviderConfig| { - Ok(Box::new(GitlabOAuthProvider::new(config)?)) - }), + name: Self::NAME, + display_name: Self::DISPLAY_NAME, + factory: Box::new(|config: &OAuthProviderConfig| Ok(Box::new(Self::new(config)?))), } } } @@ -52,13 +50,13 @@ impl GitlabOAuthProvider { #[async_trait] impl OAuthProvider for GitlabOAuthProvider { fn name(&self) -> &'static str { - GitlabOAuthProvider::NAME + Self::NAME } fn provider(&self) -> OAuthProviderId { OAuthProviderId::Gitlab } fn display_name(&self) -> &'static str { - GitlabOAuthProvider::DISPLAY_NAME + Self::DISPLAY_NAME } fn settings(&self) -> Result { @@ -81,7 +79,7 @@ impl OAuthProvider for GitlabOAuthProvider { async fn get_user(&self, access_token: String) -> Result { let response = reqwest::Client::new() - .get(GitlabOAuthProvider::USER_API_URL) + .get(Self::USER_API_URL) .bearer_auth(access_token) .send() .await diff --git a/trailbase-core/src/auth/oauth/providers/google.rs b/trailbase-core/src/auth/oauth/providers/google.rs index 228270de..ec773ca7 100644 --- a/trailbase-core/src/auth/oauth/providers/google.rs +++ b/trailbase-core/src/auth/oauth/providers/google.rs @@ -15,7 +15,7 @@ pub(crate) struct GoogleOAuthProvider { impl GoogleOAuthProvider { const NAME: &'static str = "google"; - const DISPLAY_NAME: &'static str = "google"; + const DISPLAY_NAME: &'static str = "Google"; const AUTH_URL: &'static str = "https://accounts.google.com/o/oauth2/auth"; const TOKEN_URL: &'static str = "https://accounts.google.com/o/oauth2/token"; @@ -31,7 +31,7 @@ impl GoogleOAuthProvider { )); }; - return Ok(GoogleOAuthProvider { + return Ok(Self { client_id, client_secret, }); diff --git a/trailbase-core/src/auth/oauth/providers/microsoft.rs b/trailbase-core/src/auth/oauth/providers/microsoft.rs new file mode 100644 index 00000000..2ab7f5dd --- /dev/null +++ b/trailbase-core/src/auth/oauth/providers/microsoft.rs @@ -0,0 +1,109 @@ +use async_trait::async_trait; +use lazy_static::lazy_static; +use serde::Deserialize; +use url::Url; + +use crate::auth::oauth::providers::{OAuthProviderError, OAuthProviderFactory}; +use crate::auth::oauth::{OAuthClientSettings, OAuthProvider, OAuthUser}; +use crate::auth::AuthError; +use crate::config::proto::{OAuthProviderConfig, OAuthProviderId}; + +#[derive(Default, Deserialize, Debug)] +struct MicrosoftUser { + id: String, + mail: String, +} + +pub(crate) struct MicrosoftOAuthProvider { + client_id: String, + client_secret: String, +} + +impl MicrosoftOAuthProvider { + const NAME: &'static str = "microsoft"; + const DISPLAY_NAME: &'static str = "Microsoft"; + + const AUTH_URL: &'static str = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; + const TOKEN_URL: &'static str = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; + const USER_API_URL: &'static str = "https://graph.microsoft.com/v1.0/me"; + + fn new(config: &OAuthProviderConfig) -> Result { + let Some(client_id) = config.client_id.clone() else { + return Err(OAuthProviderError::Missing( + "Microsoft client id".to_string(), + )); + }; + let Some(client_secret) = config.client_secret.clone() else { + return Err(OAuthProviderError::Missing( + "Microsoft client secret".to_string(), + )); + }; + + return Ok(Self { + client_id, + client_secret, + }); + } + + pub fn factory() -> OAuthProviderFactory { + OAuthProviderFactory { + id: OAuthProviderId::Microsoft, + name: Self::NAME, + display_name: Self::DISPLAY_NAME, + factory: Box::new(|config: &OAuthProviderConfig| Ok(Box::new(Self::new(config)?))), + } + } +} + +#[async_trait] +impl OAuthProvider for MicrosoftOAuthProvider { + fn name(&self) -> &'static str { + Self::NAME + } + fn provider(&self) -> OAuthProviderId { + OAuthProviderId::Microsoft + } + fn display_name(&self) -> &'static str { + Self::DISPLAY_NAME + } + + fn settings(&self) -> Result { + lazy_static! { + static ref AUTH_URL: Url = Url::parse(MicrosoftOAuthProvider::AUTH_URL).unwrap(); + static ref TOKEN_URL: Url = Url::parse(MicrosoftOAuthProvider::TOKEN_URL).unwrap(); + } + + return Ok(OAuthClientSettings { + auth_url: AUTH_URL.clone(), + token_url: TOKEN_URL.clone(), + client_id: self.client_id.clone(), + client_secret: self.client_secret.clone(), + }); + } + + fn oauth_scopes(&self) -> Vec<&'static str> { + return vec!["User.Read"]; + } + + async fn get_user(&self, access_token: String) -> Result { + let response = reqwest::Client::new() + .get(Self::USER_API_URL) + .bearer_auth(access_token) + .send() + .await + .map_err(|err| AuthError::FailedDependency(err.into()))?; + + let user = response + .json::() + .await + .map_err(|err| AuthError::FailedDependency(err.into()))?; + + return Ok(OAuthUser { + provider_user_id: user.id, + provider_id: OAuthProviderId::Microsoft, + email: user.mail, + verified: true, + avatar: None, + }); + } +} diff --git a/trailbase-core/src/auth/oauth/providers/mod.rs b/trailbase-core/src/auth/oauth/providers/mod.rs index 2b523c5a..a8edc0bd 100644 --- a/trailbase-core/src/auth/oauth/providers/mod.rs +++ b/trailbase-core/src/auth/oauth/providers/mod.rs @@ -1,6 +1,8 @@ mod discord; +mod facebook; mod gitlab; mod google; +mod microsoft; #[cfg(test)] pub(crate) mod test; @@ -54,6 +56,8 @@ lazy_static! { discord::DiscordOAuthProvider::factory(), gitlab::GitlabOAuthProvider::factory(), google::GoogleOAuthProvider::factory(), + facebook::FacebookOAuthProvider::factory(), + microsoft::MicrosoftOAuthProvider::factory(), #[cfg(test)] test::TestOAuthProvider::factory(), ];