feat(client >> storage): storing auth tokens in the persistent session storage system

This commit is contained in:
antifallobst 2024-03-31 23:15:28 +02:00
parent 08cf73a613
commit 3b240d6ca9
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
4 changed files with 109 additions and 42 deletions

View File

@ -7,6 +7,7 @@ pub use session::Session;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::storage::standard::StandardStorage;
use std::str::FromStr; use std::str::FromStr;
use uuid::Uuid; use uuid::Uuid;
@ -17,7 +18,23 @@ mod tests {
let token = std::env::var("ICRC_TEST_AUTH") let token = std::env::var("ICRC_TEST_AUTH")
.expect("The environment variable ICRC_TEST_AUTH needs to be set!"); .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] #[tokio::test]
@ -27,7 +44,13 @@ mod tests {
let userid = std::env::var("ICRC_TEST_USER") let userid = std::env::var("ICRC_TEST_USER")
.expect("The environment variable ICRC_TEST_USER needs to be set!"); .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 session
.auth_with_credentials(Uuid::from_str(&userid).unwrap(), "test") .auth_with_credentials(Uuid::from_str(&userid).unwrap(), "test")
.await .await

View File

@ -1,6 +1,9 @@
pub(self) mod data; pub(self) mod data;
use crate::error::Error; use crate::{
error::Error,
storage::{self, Storage, StorageError},
};
use reqwest::Response; use reqwest::Response;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::str::FromStr; use std::str::FromStr;
@ -16,52 +19,28 @@ struct AuthorizedSession {
/// An unauthorized session is not linked to any account on the server, /// An unauthorized session is not linked to any account on the server,
/// but an authorized session uses Bearer Authentication to get access to resources, /// but an authorized session uses Bearer Authentication to get access to resources,
/// which are not publicly accessible. /// which are not publicly accessible.
pub struct Session { pub struct Session<S>
where
S: Storage,
{
pub(self) host: String, pub(self) host: String,
pub(self) client: reqwest::Client, pub(self) client: reqwest::Client,
pub(self) storage: S,
pub(self) auth: Option<AuthorizedSession>, pub(self) auth: Option<AuthorizedSession>,
} }
pub(self) async fn parse_response<T: DeserializeOwned>( impl<S: Storage> Session<S> {
response: Result<Response, reqwest::Error>, /// Returns whether the session is authorized or not.
) -> Result<T, Error> { pub fn is_authorized(&self) -> bool {
match response { self.auth.is_some()
Ok(resp) => {
if resp.status().is_success() {
resp.json::<T>()
.await
.map_err(|e| Error::BadResponse(e.to_string()))
} else {
return Err(
match resp
.json::<data::api::error::Body>()
.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 { /// Creates a new [Session] with `host` as the ICRC Server host. Setting `auth` to [None] lets
/// Creates a new [Session] with `host` as the ICRC Server host. /// this function try to fetch an auth token from it's [Storage]. If this is not successful the
/// Giving `auth` the value [None] sets the session in an unauthorized state, /// session is in an unauthorized state. Setting `auth` to [`Some(token)`] calls
/// whilst [Some]`(token)` calls [Session::auth_with_token]. /// [Session::auth_with_token] (which overrides any saved auth token in the storage).
pub async fn new(mut host: &str, auth: Option<String>) -> Result<Self, Error> { pub async fn new(mut host: &str, auth: Option<String>, storage: S) -> Result<Self, Error> {
if let Some(stripped) = host.strip_suffix("/") { if let Some(stripped) = host.strip_suffix("/") {
host = stripped; host = stripped;
} }
@ -69,17 +48,34 @@ impl Session {
let mut session = Self { let mut session = Self {
host: host.to_string(), host: host.to_string(),
client: reqwest::Client::new(), client: reqwest::Client::new(),
storage,
auth: None, auth: None,
}; };
if let Some(token) = auth { if let Some(token) = auth {
session.auth_with_token(token).await?; 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) Ok(session)
} }
/// Tries to authorize the session. If `token` is not accepted by the server, this leads to an /// 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> { pub async fn auth_with_token(&mut self, token: String) -> Result<(), Error> {
let request = self let request = self
.client .client
@ -89,6 +85,18 @@ impl Session {
let response = parse_response::<data::api::account::info::Response>(request.await).await?; let response = parse_response::<data::api::account::info::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 { self.auth = Some(AuthorizedSession {
token, token,
userid: Uuid::from_str(&response.userid) userid: Uuid::from_str(&response.userid)
@ -149,3 +157,37 @@ impl Session {
Ok(()) Ok(())
} }
} }
pub(self) async fn parse_response<T: DeserializeOwned>(
response: Result<Response, reqwest::Error>,
) -> Result<T, Error> {
match response {
Ok(resp) => {
if resp.status().is_success() {
resp.json::<T>()
.await
.map_err(|e| Error::BadResponse(e.to_string()))
} else {
return Err(
match resp
.json::<data::api::error::Body>()
.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()))
}
}
}
}

View File

@ -0,0 +1 @@
pub const KEY_ACCOUNT_AUTH_TOKEN: &str = "account.auth.token";

View File

@ -1,3 +1,4 @@
pub(crate) mod keys;
pub mod standard; pub mod standard;
#[derive(Debug, thiserror::Error, Clone)] #[derive(Debug, thiserror::Error, Clone)]