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 } => {
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);
}
}