dalloriam-cloud-client 0.1.0

client library for personal cloud
Documentation
use std::str::FromStr;

use dalloriam_cloud_protocol::auth::{LoginRequest, LoginResponse, User};

use reqwest::{Client, StatusCode};
use snafu::{ensure, ResultExt, Snafu};

use crate::service::AUTH_HOST;

#[derive(Debug, Snafu)]
pub enum AuthError {
    AccessDenied,
    BadBrancaToken { source: branca::errors::Error },
    TokenDeserializationError { source: serde_json::Error },
    ResponseDeserializationError { source: reqwest::Error },
    TokenResolutionError { source: reqwest::Error },
    MalformedToken,
    UnexpectedError { status: StatusCode },
}

type Result<T> = std::result::Result<T, AuthError>;

async fn resolve_token(email: String, password: String) -> Result<String> {
    let cl = Client::default();
    let resp = cl
        .post(format!("{}/login", AUTH_HOST))
        .json(&LoginRequest { email, password })
        .send()
        .await
        .context(TokenResolutionSnafu)?;

    if !resp.status().is_success() {
        return match resp.status() {
            StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Err(AuthError::AccessDenied),
            _ => Err(AuthError::UnexpectedError {
                status: resp.status(),
            }),
        };
    }

    let data: LoginResponse = resp.json().await.context(ResponseDeserializationSnafu)?;

    Ok(data.token)
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Credentials {
    Raw { email: String, password: String },
    Token(String),
}

impl Credentials {
    pub async fn ensure_token(self) -> Result<Self> {
        match self {
            Self::Raw { email, password } => {
                let token = resolve_token(email, password).await?;
                Ok(Self::Token(token))
            }
            Self::Token(t) => Ok(Self::Token(t)),
        }
    }

    fn user_from_branca(token: String, encryption_key: String) -> Result<User> {
        let data =
            branca::decode(&token, encryption_key.as_bytes(), 3600).context(BadBrancaTokenSnafu)?;

        serde_json::from_slice(&data).context(TokenDeserializationSnafu)
    }

    pub async fn into_user(self, encryption_key: String) -> Result<User> {
        match self {
            Credentials::Token(tok) => Self::user_from_branca(tok, encryption_key),
            Credentials::Raw { email, password } => {
                // Get an auth token.
                let t = resolve_token(email, password).await?;
                Self::user_from_branca(t, encryption_key)
            }
        }
    }
}

impl ToString for Credentials {
    fn to_string(&self) -> String {
        match self {
            Self::Raw { email, password } => {
                let encoded = base64::encode(format!("{}:{}", email, password));
                format!("Basic {}", encoded)
            }
            Self::Token(token) => format!("Bearer {}", token),
        }
    }
}

impl FromStr for Credentials {
    type Err = AuthError;

    fn from_str(s: &str) -> Result<Self> {
        if s.starts_with("Bearer ") {
            let tok = s.strip_prefix("Bearer ").ok_or(AuthError::MalformedToken)?;
            Ok(Credentials::Token(String::from(tok)))
        } else if s.starts_with("Basic ") {
            let basic_part = s.strip_prefix("Basic ").ok_or(AuthError::MalformedToken)?;
            let basic_decoded =
                base64::decode(&basic_part).map_err(|_| AuthError::MalformedToken)?;
            let basic_decoded_str = String::from_utf8_lossy(&basic_decoded);

            let split = basic_decoded_str.splitn(2, ':').collect::<Vec<_>>();
            ensure!(split.len() == 2, MalformedTokenSnafu);
            Ok(Credentials::Raw {
                email: String::from(split[0]),
                password: String::from(split[1]),
            })
        } else {
            Err(AuthError::MalformedToken)
        }
    }
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use super::Credentials;

    #[test]
    fn parse_basic_auth() {
        let actual = Credentials::from_str("Basic aGVsbG9AdXNlci5jb206YXNkZmFzZGY=").unwrap();
        let expected = Credentials::Raw {
            email: "hello@user.com".to_string(),
            password: "asdfasdf".to_string(),
        };

        assert_eq!(expected, actual);
    }

    #[test]
    fn parse_bearer_auth() {
        let actual = Credentials::from_str("Bearer asdfasdf").unwrap();
        let expected = Credentials::Token("asdfasdf".to_string());

        assert_eq!(expected, actual);
    }
}