use crate::api_account_claims;
use crate::client::{OrdinaryApiClient, compress_zstd, get_client_dir, strip_http};
use anyhow::bail;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use bytes::{Bytes, BytesMut};
use ed25519_dalek::SigningKey;
use ordinary_auth::token::check_exp;
use ordinary_auth::{AuthClient, EXP_LEN, OsRng};
use ordinary_config::OrdinaryConfig;
use ordinary_types::flexbuffer_reader_to_json;
use serde_json::Value;
use sha2::{Digest, Sha256};
use totp_rs::TOTP;
pub async fn register(
api_client: &OrdinaryApiClient<'_>,
password: &str,
invite_token: &str,
) -> anyhow::Result<(TOTP, String)> {
let correlation_id = api_client
.correlation_id
.unwrap_or(uuid::Uuid::new_v4())
.to_string();
let (password, domain) = api_client.get_password_hash_and_domain(password);
let (state, registration_start_req) =
AuthClient::registration_start_req(api_client.account.as_bytes(), &password)?;
tracing::info!("registration starting...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/registration/start",
api_client.addr
))
.body(compress_zstd(®istration_start_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.header("Authorization", format!("Bearer {invite_token}"))
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'registration start' request. status: {}",
res.status()
);
}
let registration_start_res = res.bytes().await?;
let (private_key, registration_finish_req) = AuthClient::registration_finish_req(
api_client.account.as_bytes(),
&password,
&state,
®istration_start_res,
)?;
tracing::info!("finishing registration...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/registration/finish",
api_client.addr
))
.body(compress_zstd(®istration_finish_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'registration finish' request. status: {}",
res.status()
);
}
let registration_finish_res = res.bytes().await?;
tracing::info!("registration finished.");
let (totp, recovery_codes_str) = AuthClient::decrypt_totp_mfa(
®istration_finish_res,
private_key,
domain.into(),
api_client.account.into(),
)?;
Ok((totp, recovery_codes_str))
}
pub async fn login(
api_client: &OrdinaryApiClient<'_>,
password: &str,
mfa_code: &str,
) -> anyhow::Result<()> {
let correlation_id = api_client
.correlation_id
.unwrap_or(uuid::Uuid::new_v4())
.to_string();
let (password, _) = api_client.get_password_hash_and_domain(password);
let (mfa_code, domain) = api_client.get_password_hash_and_domain(mfa_code);
let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let (state, login_start_req) =
AuthClient::login_start_req(api_client.account.as_bytes(), &password)?;
tracing::info!("login starting...");
let res = api_client
.client
.post(format!("{}/v1/accounts/login/start", api_client.addr))
.body(compress_zstd(&login_start_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!("failed 'login start' request. status: {}", res.status());
}
let login_start_res = res.bytes().await?;
let (login_finish_req, session_key) = AuthClient::login_finish_req(
api_client.account.as_bytes(),
&password,
&mfa_code,
&state,
&login_start_res,
Some(verifying_key.as_bytes()),
)?;
tracing::info!("finishing login...");
let res = api_client
.client
.post(format!("{}/v1/accounts/login/finish", api_client.addr))
.body(compress_zstd(&login_finish_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!("failed 'login finish' request. status: {}", res.status());
}
let login_finish_res = res.bytes().await?;
tracing::info!("login finished.");
let refresh_token = AuthClient::decrypt_token(&login_finish_res, &session_key)?;
tracing::info!("writing refresh token to ./.ordinary/clients...");
let client_dir = get_client_dir(domain, api_client.account);
fs_err::create_dir_all(&client_dir)?;
fs_err::write(client_dir.join("host"), strip_http(api_client.addr))?;
fs_err::write(client_dir.join("refresh_token"), refresh_token)?;
fs_err::write(client_dir.join("signing_key"), signing_key.as_bytes())?;
let access_token_path = client_dir.join("access_token");
if fs_err::read(&access_token_path).is_ok() {
fs_err::remove_file(access_token_path)?;
}
api_client.get_access(None, Some(correlation_id)).await?;
tracing::info!("logged in.");
Ok(())
}
pub async fn get_access(
api_client: &OrdinaryApiClient<'_>,
duration_s: Option<u32>,
correlation_id: Option<String>,
) -> anyhow::Result<Vec<u8>> {
tracing::info!("checking for access token...");
let domain = match api_client.api_domain {
Some(domain) => domain,
None => strip_http(api_client.addr),
};
let client_dir = get_client_dir(domain, api_client.account);
let access_token = fs_err::read(client_dir.join("access_token"));
let need_new_token = if let Ok(access_token) = &access_token {
if access_token.len() < EXP_LEN {
tracing::warn!("access token doesn't have expiration; fetching...");
true
} else if !check_exp(access_token)? {
tracing::warn!("access token expired; refetching...");
true
} else {
false
}
} else {
tracing::warn!("access token not present; fetching...");
true
};
let mut access_token: BytesMut = if need_new_token {
let mut refresh_token: BytesMut =
Bytes::copy_from_slice(&fs_err::read(client_dir.join("refresh_token"))?[..]).into();
if !check_exp(&refresh_token[..])? {
bail!("expired refresh token. login again");
}
OrdinaryApiClient::sign_token(&client_dir, &mut refresh_token, None, None)?;
let mut req = api_client
.client
.get(format!("{}/v1/accounts/access", api_client.addr))
.header(
"Authorization",
format!("Bearer {}", b64.encode(&refresh_token)),
);
if let Some(correlation_id) = correlation_id {
req = req.header("x-correlation-id", correlation_id);
}
let res = req.send().await?;
if !res.status().is_success() {
bail!("failed 'access' request. status: {}", res.status());
}
let access_token = res.bytes().await?;
tracing::info!("writing access token to ./.ordinary/clients...");
fs_err::write(client_dir.join("access_token"), &access_token)?;
tracing::info!("access granted.");
access_token
} else {
tracing::info!("access granted.");
let token = access_token?;
Bytes::copy_from_slice(&token[..])
}
.into();
OrdinaryApiClient::sign_token(&client_dir, &mut access_token, None, duration_s)?;
Ok(access_token.into())
}
#[allow(clippy::too_many_lines)]
pub async fn reset_password(
api_client: &OrdinaryApiClient<'_>,
old_password: &str,
mfa_code: &str,
new_password: &str,
) -> anyhow::Result<()> {
let correlation_id = api_client
.correlation_id
.unwrap_or(uuid::Uuid::new_v4())
.to_string();
let (old_password, domain) = api_client.get_password_hash_and_domain(old_password);
let (mfa_code, _) = api_client.get_password_hash_and_domain(mfa_code);
tracing::info!("resetting password...");
let (state, login_start_req) =
AuthClient::reset_password_login_start_req(api_client.account.as_bytes(), &old_password)?;
tracing::info!("reset login starting...");
let login_start_res = api_client
.client
.post(format!(
"{}/v1/accounts/password/reset/login/start",
api_client.addr
))
.body(compress_zstd(&login_start_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?
.bytes()
.await?;
let signing_key: SigningKey = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let (login_finish_req, session_key) = AuthClient::reset_password_login_finish_req(
api_client.account.as_bytes(),
&old_password,
&mfa_code,
&state,
&login_start_res,
Some(verifying_key.as_bytes()),
)?;
tracing::info!("finishing reset login...");
let login_finish_res = api_client
.client
.post(format!(
"{}/v1/accounts/password/reset/login/finish",
api_client.addr
))
.body(compress_zstd(&login_finish_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?
.bytes()
.await?;
tracing::info!("reset login finished.");
let mut reset_token: BytesMut =
AuthClient::decrypt_token(&login_finish_res, &session_key)?.into();
let client_dir = get_client_dir(domain, api_client.account);
OrdinaryApiClient::sign_token(&client_dir, &mut reset_token, Some(signing_key), None)?;
let (new_password, _) = api_client.get_password_hash_and_domain(new_password);
let (state, registration_start_req) = AuthClient::password_reset_registration_start_req(
api_client.account.as_bytes(),
&new_password,
)?;
tracing::info!("reset re-registration starting...");
let registration_start_res = api_client
.client
.post(format!(
"{}/v1/accounts/password/reset/registration/start",
api_client.addr
))
.body(compress_zstd(®istration_start_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.header(
"Authorization",
format!("Bearer {}", b64.encode(&reset_token[..])),
)
.send()
.await?
.bytes()
.await?;
let registration_finish_req = AuthClient::password_reset_registration_finish_req(
api_client.account.as_bytes(),
&new_password,
&state,
®istration_start_res,
)?;
tracing::info!("finishing reset re-registration...");
api_client
.client
.post(format!(
"{}/v1/accounts/password/reset/registration/finish",
api_client.addr
))
.body(compress_zstd(®istration_finish_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.header(
"Authorization",
format!("Bearer {}", b64.encode(&reset_token[..])),
)
.send()
.await?
.bytes()
.await?;
tracing::info!("reset re-registration finished.");
Ok(())
}
pub async fn forgot_password(
api_client: &OrdinaryApiClient<'_>,
password: &str,
recovery_code: &str,
) -> anyhow::Result<()> {
let correlation_id = api_client
.correlation_id
.unwrap_or(uuid::Uuid::new_v4())
.to_string();
let (password, _) = api_client.get_password_hash_and_domain(password);
let mut hasher = Sha256::new();
hasher.update(recovery_code.as_bytes());
let hashed_recovery_code = hasher.finalize().to_vec();
let (state, forgot_password_start_req) = AuthClient::forgot_password_start_req(
api_client.account.as_bytes(),
&password,
&hashed_recovery_code,
)?;
tracing::info!("forgot password starting...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/password/forgot/start",
api_client.addr
))
.body(compress_zstd(&forgot_password_start_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'forgot password start' request. status: {}",
res.status()
);
}
let forgot_password_start_res = res.bytes().await?;
let forgot_password_finish_req = AuthClient::forgot_password_finish_req(
api_client.account.as_bytes(),
&password,
&state,
&forgot_password_start_res,
recovery_code.as_bytes(),
)?;
tracing::info!("finishing forgot password...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/password/forgot/finish",
api_client.addr
))
.body(compress_zstd(&forgot_password_finish_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'forgot password finish' request. status: {}",
res.status()
);
}
tracing::info!("forgot password finished.");
Ok(())
}
pub async fn mfa_totp_reset(
api_client: &OrdinaryApiClient<'_>,
password: &str,
mfa_code: &str,
) -> anyhow::Result<TOTP> {
let correlation_id = api_client
.correlation_id
.unwrap_or(uuid::Uuid::new_v4())
.to_string();
let (password, _) = api_client.get_password_hash_and_domain(password);
let (mfa_code, domain) = api_client.get_password_hash_and_domain(mfa_code);
let (state, totp_mfa_reset_start_req) =
AuthClient::reset_totp_mfa_start_req(api_client.account.as_bytes(), &password)?;
tracing::info!("MFA TOTP reset starting...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/mfa/totp/reset/start",
api_client.addr
))
.body(compress_zstd(&totp_mfa_reset_start_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'MFA TOTP reset start' request. status: {}",
res.status()
);
}
let mfa_totp_reset_start_res = res.bytes().await?;
let (mfa_totp_reset_finish_req, session_key) = AuthClient::reset_totp_mfa_finish_req(
api_client.account.as_bytes(),
&password,
&mfa_code,
&state,
&mfa_totp_reset_start_res,
)?;
tracing::info!("finishing MFA TOTP reset...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/mfa/totp/reset/finish",
api_client.addr
))
.body(compress_zstd(&mfa_totp_reset_finish_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'MFA TOTP reset finish' request. status: {}",
res.status()
);
}
let mfa_totp_reset_finish_res = res.bytes().await?;
tracing::info!("MFA TOTP reset finished.");
let totp = AuthClient::decrypt_reset_totp_mfa(
&mfa_totp_reset_finish_res,
&session_key,
domain.into(),
api_client.account.into(),
)?;
Ok(totp)
}
pub async fn mfa_totp_lost(
api_client: &OrdinaryApiClient<'_>,
password: &str,
recovery_code: &str,
) -> anyhow::Result<TOTP> {
let correlation_id = api_client
.correlation_id
.unwrap_or(uuid::Uuid::new_v4())
.to_string();
let (password, domain) = api_client.get_password_hash_and_domain(password);
let mut hasher = Sha256::new();
hasher.update(recovery_code.as_bytes());
let hashed_recovery_code = hasher.finalize().to_vec();
let (state, totp_mfa_lost_start_req) = AuthClient::lost_totp_mfa_start_req(
api_client.account.as_bytes(),
&password,
&hashed_recovery_code,
)?;
tracing::info!("MFA TOTP lost starting...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/mfa/totp/lost/start",
api_client.addr
))
.body(compress_zstd(&totp_mfa_lost_start_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'MFA TOTP lost start' request. status: {}",
res.status()
);
}
let mfa_totp_lost_start_res = res.bytes().await?;
let (mfa_totp_lost_finish_req, session_key) = AuthClient::lost_totp_mfa_finish_req(
api_client.account.as_bytes(),
&password,
&state,
&mfa_totp_lost_start_res,
recovery_code.as_bytes(),
)?;
tracing::info!("finishing MFA TOTP lost...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/mfa/totp/lost/finish",
api_client.addr
))
.body(compress_zstd(&mfa_totp_lost_finish_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'MFA TOTP lost finish' request. status: {}",
res.status()
);
}
let mfa_totp_reset_finish_res = res.bytes().await?;
tracing::info!("MFA TOTP reset finished.");
let totp = AuthClient::decrypt_lost_totp_mfa(
&mfa_totp_reset_finish_res,
&session_key,
domain.into(),
api_client.account.into(),
)?;
Ok(totp)
}
pub async fn recovery_codes_reset(
api_client: &OrdinaryApiClient<'_>,
password: &str,
mfa_code: &str,
) -> anyhow::Result<String> {
let correlation_id = api_client
.correlation_id
.unwrap_or(uuid::Uuid::new_v4())
.to_string();
let (password, _) = api_client.get_password_hash_and_domain(password);
let (mfa_code, _) = api_client.get_password_hash_and_domain(mfa_code);
let (state, totp_mfa_lost_start_req) =
AuthClient::reset_recovery_codes_start_req(api_client.account.as_bytes(), &password)?;
tracing::info!("recovery codes reset starting...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/recovery-codes/reset/start",
api_client.addr
))
.body(compress_zstd(&totp_mfa_lost_start_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'recovery codes reset start' request. status: {}",
res.status()
);
}
let mfa_totp_lost_start_res = res.bytes().await?;
let (mfa_totp_lost_finish_req, session_key) = AuthClient::reset_recovery_codes_finish_req(
api_client.account.as_bytes(),
&password,
&mfa_code,
&state,
&mfa_totp_lost_start_res,
)?;
tracing::info!("finishing recovery codes reset...");
let res = api_client
.client
.post(format!(
"{}/v1/accounts/recovery-codes/reset/finish",
api_client.addr
))
.body(compress_zstd(&mfa_totp_lost_finish_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!(
"failed 'recovery codes reset finish' status: {}",
res.status()
);
}
let mfa_totp_reset_finish_res = res.bytes().await?;
tracing::info!("recovery codes reset finished.");
let recovery_codes =
AuthClient::decrypt_reset_recovery_codes(&mfa_totp_reset_finish_res, &session_key)?;
Ok(recovery_codes)
}
pub async fn delete(
api_client: &OrdinaryApiClient<'_>,
password: &str,
mfa_code: &str,
) -> anyhow::Result<()> {
let correlation_id = api_client
.correlation_id
.unwrap_or(uuid::Uuid::new_v4())
.to_string();
let (password, _) = api_client.get_password_hash_and_domain(password);
let (mfa_code, _) = api_client.get_password_hash_and_domain(mfa_code);
let (state, delete_start_req) =
AuthClient::delete_account_start_req(api_client.account.as_bytes(), &password)?;
tracing::info!("delete starting...");
let res = api_client
.client
.post(format!("{}/v1/accounts/delete/start", api_client.addr))
.body(compress_zstd(&delete_start_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!("failed 'delete start' request. status: {}", res.status());
}
let delete_start_res = res.bytes().await?;
let delete_finish_req = AuthClient::delete_account_finish_req(
api_client.account.as_bytes(),
&password,
&mfa_code,
&state,
&delete_start_res,
)?;
tracing::info!("finishing delete...");
let res = api_client
.client
.post(format!("{}/v1/accounts/delete/finish", api_client.addr))
.body(compress_zstd(&delete_finish_req[..])?)
.header("Content-Encoding", "zstd")
.header("x-correlation-id", correlation_id.as_str())
.send()
.await?;
if !res.status().is_success() {
bail!("failed 'delete finish' request. status: {}", res.status());
}
tracing::info!("account deleted.");
Ok(())
}
pub async fn invite_account(
api_client: &OrdinaryApiClient<'_>,
app_domain: &str,
account_name: &str,
permissions: Vec<u8>,
) -> anyhow::Result<String> {
let access_token = api_client
.get_access(None, api_client.correlation_id.map(|id| id.to_string()))
.await?;
tracing::info!("inviting API account...");
let mut params = vec![
("d", app_domain.to_string()),
("a", account_name.to_string()),
];
for permission in permissions
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
{
params.push(("p", permission));
}
let mut req = api_client
.client
.get(format!("{}/v1/accounts/invite", api_client.addr))
.query(¶ms)
.header("Content-Encoding", "zstd")
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
);
if let Some(correlation_id) = api_client.correlation_id {
req = req.header("x-correlation-id", correlation_id.to_string());
}
let res = req.send().await?;
if !res.status().is_success() {
bail!("failed 'invite' request. status: {}", res.status());
}
let invite_token = res.bytes().await?;
Ok(b64.encode(invite_token))
}
pub async fn accounts_list(
api_client: &OrdinaryApiClient<'_>,
proj_path: Option<&str>,
) -> anyhow::Result<String> {
let mut params = vec![];
if let Some(proj_path) = proj_path {
let config = OrdinaryConfig::get(proj_path)?;
params.push(("d", config.domain));
}
let access_token = api_client
.get_access(None, api_client.correlation_id.map(|id| id.to_string()))
.await?;
tracing::info!("fetching...");
let mut req = api_client
.client
.get(format!("{}/v1/accounts", api_client.addr))
.query(¶ms)
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
);
if let Some(correlation_id) = api_client.correlation_id {
req = req.header("x-correlation-id", correlation_id.to_string());
}
let res = req.send().await?.bytes().await?;
let root = flexbuffers::Reader::get_root(&res[..])?;
let mut out = vec![];
for account in &root.as_vector() {
let mut account_out = vec![];
let account_name_bytes = account
.as_vector()
.idx(0)
.as_vector()
.iter()
.map(|v| v.as_u8())
.collect::<Vec<u8>>();
let account_name = std::str::from_utf8(&account_name_bytes)?;
account_out.push(Value::from(account_name));
for claim_field in &api_account_claims() {
let json_val = flexbuffer_reader_to_json(
&claim_field.kind,
&account.as_vector().idx(claim_field.idx as usize),
)?;
account_out.push(json_val);
}
out.push(account_out);
}
tracing::info!("done.");
Ok(serde_json::to_string(&out)?)
}