diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/api-auth/src/discord/mod.rs | 101 | ||||
| -rw-r--r-- | crates/api-auth/src/error.rs | 10 | ||||
| -rw-r--r-- | crates/api-auth/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/api-auth/src/util.rs | 103 |
4 files changed, 139 insertions, 77 deletions
diff --git a/crates/api-auth/src/discord/mod.rs b/crates/api-auth/src/discord/mod.rs index 0844f58..43a62bf 100644 --- a/crates/api-auth/src/discord/mod.rs +++ b/crates/api-auth/src/discord/mod.rs @@ -1,7 +1,7 @@ use api_core::models::user::User; use async_session::{Session, serde_json}; use async_trait::async_trait; -use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse}; +use oauth2::{AuthorizationCode, CsrfToken, TokenResponse}; use redis::AsyncCommands; use serde::{Deserialize, Serialize}; use sh_util::cache::{CacheKey, RedisManager}; @@ -19,11 +19,19 @@ struct DiscordUser { avatar: Option<String>, username: String, discriminator: String, + email: Option<String>, + verified: bool, } -impl From<DiscordUser> for User { - fn from(value: DiscordUser) -> Self { - todo!() +impl TryFrom<DiscordUser> for User { + type Error = AuthError; + + fn try_from(user_data: DiscordUser) -> Result<Self, Self::Error> { + match (&user_data.email, user_data.verified) { + (None, _) => Err(AuthError::MissingEmail), + (_, false) => Err(AuthError::EmailNotVerified), + (Some(_), true) => Ok(Self {}), + } } } @@ -47,84 +55,23 @@ impl AuthServiceDiscord { #[async_trait] impl OauthDriver for AuthServiceDiscord { async fn get_user(&self, client: &AuthHttpClient, code: &str) -> Result<User, AuthError> { - // Get an auth token - let token = self - .client - .exchange_code(AuthorizationCode::new(code.to_owned())) - .request_async(client) - .await - .unwrap(); - // Fetch user data from discord - let user_data: DiscordUser = client - // https://discord.com/developers/docs/resources/user#get-current-user - .get("https://discordapp.com/api/users/@me") - .bearer_auth(token.access_token().secret()) - .send() - .await - .unwrap() - .json::<DiscordUser>() - .await - .unwrap(); - - Ok(user_data.into()) + crate::util::get_user::<DiscordUser>( + &self.client, + client, + code, + "https://discordapp.com/api/users/@me", + ) + .await } - async fn validate_session(&self, cookie: &str, state: &str) -> Result<(), AuthError> { - let id = Session::id_from_cookie_value(cookie)?; - let cache_key = CacheKey::Session(&id); - let mut cache = self.cache.get().await.unwrap(); - let session = cache.get::<_, String>(&cache_key).await?; - let session: Session = - serde_json::from_str(&session).map_err(|_e| AuthError::InvalidSession)?; - - match session.validate() { - Some(session) => { - // Extract the CSRF token from the session - let stored_csrf_token = session.get::<CsrfToken>(CSRF_TOKEN); - - if let Some(stored) = stored_csrf_token { - // Cleanup the CSRF token session - cache.del::<_, ()>(cache_key).await?; - // Validate CSRF token is the same as the one in the auth request - if *stored.secret() != state { - return Err(AuthError::TokenMismatch); - } else { - return Ok(()); - } - } else { - return Err(AuthError::NoCSRFToken); - } - } - None => return Err(AuthError::MissingSession), - } + async fn validate_session(&self, cookie: &str, state: &str) -> Result<(), AuthError> { + crate::util::validate_session(&self.cache, cookie, state).await } - async fn create_oauth_session(&self) -> Result<SessionResponse, AuthError> { - let (auth_url, csrf_token) = self - .client - .authorize_url(CsrfToken::new_random) - .add_scope(Scope::new("identify".to_string())) - .url(); - - let mut session = Session::new(); - session.insert(CSRF_TOKEN, &csrf_token).unwrap(); - let cache_key = CacheKey::Session(session.id()); - let mut cache = self.cache.get().await.unwrap(); - cache - .set::<_, _, ()>( - cache_key, - serde_json::to_string(&session).or(Err(AuthError::InvalidSession))?, - ) - .await?; - let cookie = session - .into_cookie_value() - .ok_or(AuthError::MissingSession)?; - - Ok(SessionResponse { - cookie_value: cookie, - auth_url, - }) + async fn create_oauth_session(&self) -> Result<SessionResponse, AuthError> { + crate::util::create_oauth_session(&self.client, &self.cache, &["identify", "email"]).await } + async fn save_session(&self, user: &User) -> Result<(), AuthError> { todo!() } diff --git a/crates/api-auth/src/error.rs b/crates/api-auth/src/error.rs index 2db3281..86da20c 100644 --- a/crates/api-auth/src/error.rs +++ b/crates/api-auth/src/error.rs @@ -35,4 +35,14 @@ pub enum AuthError { TokenMismatch, #[error("CSRF token missing")] NoCSRFToken, + #[error("No email available for this user")] + MissingEmail, + #[error("Email is not verified")] + EmailNotVerified, + #[error("oauth token for user")] + UserToken, + #[error("could not get user through http")] + UserRetrieval, + #[error("remote user mismatch")] + UserDeserialisation, } diff --git a/crates/api-auth/src/lib.rs b/crates/api-auth/src/lib.rs index 815b170..24b966c 100644 --- a/crates/api-auth/src/lib.rs +++ b/crates/api-auth/src/lib.rs @@ -2,6 +2,7 @@ pub mod discord; pub mod client; +pub(crate) mod util; mod error; use api_core::auth::AuthClientConfig; @@ -39,6 +40,7 @@ use url::Url; use crate::error::AuthError; +#[allow(dead_code)] static CSRF_TOKEN: &str = "csrf_token"; pub struct SessionResponse { diff --git a/crates/api-auth/src/util.rs b/crates/api-auth/src/util.rs new file mode 100644 index 0000000..0893bd5 --- /dev/null +++ b/crates/api-auth/src/util.rs @@ -0,0 +1,103 @@ +use api_core::models::user::User; +use async_session::{Session, serde_json}; +use oauth2::{AuthorizationCode, CsrfToken, Scope, TokenResponse}; +use redis::AsyncCommands; +use serde::{Deserialize, de::DeserializeOwned}; +use sh_util::cache::{CacheKey, RedisManager}; + +use crate::{BasicClient, CSRF_TOKEN, SessionResponse, client::AuthHttpClient, error::AuthError}; + +pub async fn create_oauth_session( + client: &BasicClient, + cache: &RedisManager, + scopes: &[&str], +) -> Result<SessionResponse, AuthError> { + let mut builder = client.authorize_url(CsrfToken::new_random); + + for pat in scopes { + builder = builder.add_scope(Scope::new(pat.to_string())); + } + let (auth_url, csrf_token) = builder.url(); + + let mut session = Session::new(); + session.insert(CSRF_TOKEN, &csrf_token).unwrap(); + + let cache_key = CacheKey::Session(session.id()); + let mut cache = cache.get().await.unwrap(); + cache + .set::<_, _, ()>( + cache_key, + serde_json::to_string(&session).or(Err(AuthError::InvalidSession))?, + ) + .await?; + let cookie = session + .into_cookie_value() + .ok_or(AuthError::MissingSession)?; + + Ok(SessionResponse { + cookie_value: cookie, + auth_url, + }) +} + +pub async fn get_user<T>( + c: &BasicClient, + client: &AuthHttpClient, + code: &str, + endpoint: &str, +) -> Result<User, AuthError> +where + User: TryFrom<T>, + T: DeserializeOwned, +{ + // Get an auth token + let token = c + .exchange_code(AuthorizationCode::new(code.to_owned())) + .request_async(client) + .await + .map_err(|_e| AuthError::UserToken)?; + // Fetch user data from discord + let user_data: T = client + // https://discord.com/developers/docs/resources/user#get-current-user + .get("https://discordapp.com/api/users/@me") + .bearer_auth(token.access_token().secret()) + .send() + .await + .map_err(|_e| AuthError::UserRetrieval)? + .json::<T>() + .await + .map_err(|_e| AuthError::UserDeserialisation)?; + + User::try_from(user_data).map_err(|_e| AuthError::UserDeserialisation) +} + + pub async fn validate_session(cache: &RedisManager, cookie: &str, state: &str) -> Result<(), AuthError> { + let id = Session::id_from_cookie_value(cookie)?; + let cache_key = CacheKey::Session(&id); + let mut cache = cache.get().await.unwrap(); + let session = cache.get::<_, String>(&cache_key).await?; + let session: Session = + serde_json::from_str(&session).map_err(|_e| AuthError::InvalidSession)?; + + match session.validate() { + Some(session) => { + // Extract the CSRF token from the session + let stored_csrf_token = session.get::<CsrfToken>(CSRF_TOKEN); + + if let Some(stored) = stored_csrf_token { + // Cleanup the CSRF token session + cache.del::<_, ()>(cache_key).await?; + + // Validate CSRF token is the same as the one in the auth request + if *stored.secret() != state { + Err(AuthError::TokenMismatch) + } else { + Ok(()) + } + } else { + Err(AuthError::NoCSRFToken) + } + } + None => Err(AuthError::MissingSession), + } + } |
