From 3b240d6ca9ade93d055c4ef68d16761ae6ca0e84 Mon Sep 17 00:00:00 2001 From: antifallobst Date: Sun, 31 Mar 2024 23:15:28 +0200 Subject: [PATCH] feat(client >> storage): storing auth tokens in the persistent session storage system --- client/src/lib.rs | 27 +++++++- client/src/session/mod.rs | 122 +++++++++++++++++++++++++------------ client/src/storage/keys.rs | 1 + client/src/storage/mod.rs | 1 + 4 files changed, 109 insertions(+), 42 deletions(-) create mode 100644 client/src/storage/keys.rs diff --git a/client/src/lib.rs b/client/src/lib.rs index 1109edb..5d16c56 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -7,6 +7,7 @@ pub use session::Session; #[cfg(test)] mod tests { use super::*; + use crate::storage::standard::StandardStorage; use std::str::FromStr; use uuid::Uuid; @@ -17,7 +18,23 @@ mod tests { let token = std::env::var("ICRC_TEST_AUTH") .expect("The environment variable ICRC_TEST_AUTH needs to be set!"); - let session = Session::new(&host, Some(token)).await.unwrap(); + let _session = Session::new( + &host, + Some(token), + StandardStorage::new("sqlite:test.db").await.unwrap(), + ) + .await + .unwrap(); + + let new_session = Session::new( + &host, + None, + StandardStorage::new("sqlite:test.db").await.unwrap(), + ) + .await + .unwrap(); + + assert!(new_session.is_authorized()) } #[tokio::test] @@ -27,7 +44,13 @@ mod tests { let userid = std::env::var("ICRC_TEST_USER") .expect("The environment variable ICRC_TEST_USER needs to be set!"); - let mut session = Session::new(&host, None).await.unwrap(); + let mut session = Session::new( + &host, + None, + StandardStorage::new("sqlite::memory:").await.unwrap(), + ) + .await + .unwrap(); session .auth_with_credentials(Uuid::from_str(&userid).unwrap(), "test") .await diff --git a/client/src/session/mod.rs b/client/src/session/mod.rs index 92e1814..b6110e7 100644 --- a/client/src/session/mod.rs +++ b/client/src/session/mod.rs @@ -1,6 +1,9 @@ pub(self) mod data; -use crate::error::Error; +use crate::{ + error::Error, + storage::{self, Storage, StorageError}, +}; use reqwest::Response; use serde::de::DeserializeOwned; use std::str::FromStr; @@ -16,52 +19,28 @@ struct AuthorizedSession { /// An unauthorized session is not linked to any account on the server, /// but an authorized session uses Bearer Authentication to get access to resources, /// which are not publicly accessible. -pub struct Session { +pub struct Session +where + S: Storage, +{ pub(self) host: String, pub(self) client: reqwest::Client, + pub(self) storage: S, pub(self) auth: Option, } -pub(self) async fn parse_response( - response: Result, -) -> Result { - match response { - Ok(resp) => { - if resp.status().is_success() { - resp.json::() - .await - .map_err(|e| Error::BadResponse(e.to_string())) - } else { - return Err( - match resp - .json::() - .await - .map_err(|e| Error::BadResponse(e.to_string()))? - .error - { - data::api::error::Error::InvalidToken => Error::InvalidToken, - data::api::error::Error::TokenExpired => Error::TokenExpired, - e => Error::BadResponse(format!("Unknown Error/{e} in this context")), - }, - ); - } - } - Err(e) => { - if e.is_connect() || e.is_timeout() { - Err(Error::ConnectionError) - } else { - Err(Error::Unknown(e.to_string())) - } - } +impl Session { + /// Returns whether the session is authorized or not. + pub fn is_authorized(&self) -> bool { + self.auth.is_some() } -} -impl Session { - /// Creates a new [Session] with `host` as the ICRC Server host. - /// Giving `auth` the value [None] sets the session in an unauthorized state, - /// whilst [Some]`(token)` calls [Session::auth_with_token]. - pub async fn new(mut host: &str, auth: Option) -> Result { + /// Creates a new [Session] with `host` as the ICRC Server host. Setting `auth` to [None] lets + /// this function try to fetch an auth token from it's [Storage]. If this is not successful the + /// session is in an unauthorized state. Setting `auth` to [`Some(token)`] calls + /// [Session::auth_with_token] (which overrides any saved auth token in the storage). + pub async fn new(mut host: &str, auth: Option, storage: S) -> Result { if let Some(stripped) = host.strip_suffix("/") { host = stripped; } @@ -69,17 +48,34 @@ impl Session { let mut session = Self { host: host.to_string(), client: reqwest::Client::new(), + storage, auth: None, }; + if let Some(token) = auth { session.auth_with_token(token).await?; + } else { + match session + .storage + .pull(storage::keys::KEY_ACCOUNT_AUTH_TOKEN) + .await + { + Ok(data) => { + let token = String::from_utf8(data.to_owned()) + .map_err(|e| Error::Unknown(e.to_string()))?; + session.auth_with_token(token).await? + } + Err(StorageError::IdNotFound(_)) => (), + Err(e) => return Err(Error::Unknown(e.to_string())), + } } Ok(session) } /// Tries to authorize the session. If `token` is not accepted by the server, this leads to an - /// [crate::error::Error::InvalidToken] or [crate::error::Error::TokenExpired] error. + /// [crate::error::Error::InvalidToken] or [crate::error::Error::TokenExpired] error. If the + /// session is successfully authorized, the auth token is saved in the sessions [Storage]. pub async fn auth_with_token(&mut self, token: String) -> Result<(), Error> { let request = self .client @@ -89,6 +85,18 @@ impl Session { let response = parse_response::(request.await).await?; + match self + .storage + .push( + storage::keys::KEY_ACCOUNT_AUTH_TOKEN, + token.to_owned().into_bytes(), + ) + .await + { + Ok(_) | Err(StorageError::IdCollision(_)) => (), + Err(e) => return Err(Error::Unknown(e.to_string())), + } + self.auth = Some(AuthorizedSession { token, userid: Uuid::from_str(&response.userid) @@ -149,3 +157,37 @@ impl Session { Ok(()) } } + +pub(self) async fn parse_response( + response: Result, +) -> Result { + match response { + Ok(resp) => { + if resp.status().is_success() { + resp.json::() + .await + .map_err(|e| Error::BadResponse(e.to_string())) + } else { + return Err( + match resp + .json::() + .await + .map_err(|e| Error::BadResponse(e.to_string()))? + .error + { + data::api::error::Error::InvalidToken => Error::InvalidToken, + data::api::error::Error::TokenExpired => Error::TokenExpired, + e => Error::BadResponse(format!("Unknown Error/{e} in this context")), + }, + ); + } + } + Err(e) => { + if e.is_connect() || e.is_timeout() { + Err(Error::ConnectionError) + } else { + Err(Error::Unknown(e.to_string())) + } + } + } +} diff --git a/client/src/storage/keys.rs b/client/src/storage/keys.rs new file mode 100644 index 0000000..5459574 --- /dev/null +++ b/client/src/storage/keys.rs @@ -0,0 +1 @@ +pub const KEY_ACCOUNT_AUTH_TOKEN: &str = "account.auth.token"; diff --git a/client/src/storage/mod.rs b/client/src/storage/mod.rs index 95833c4..decf704 100644 --- a/client/src/storage/mod.rs +++ b/client/src/storage/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod keys; pub mod standard; #[derive(Debug, thiserror::Error, Clone)]