pub mod payload;
use std::path::PathBuf;
use jiff::ToSpan;
use payload::response::WebAuthnCredential;
use poem_openapi::OpenApi;
use poem_openapi::payload::{Json, PlainText};
use webauthn_rs::Webauthn;
use crate::SaneName;
use crate::io::file::UserDirectory;
use self::payload::jwt::{UserToken, check_jwt, make_jwt};
use self::payload::request::{self as req};
use self::payload::response::{self as resp};
pub struct AuthApi {
storage: UserDirectory,
webauthn: Webauthn,
}
#[OpenApi]
impl AuthApi {
pub fn new(storage: PathBuf, webauthn: Webauthn) -> Self {
Self {
storage: UserDirectory::new(storage),
webauthn,
}
}
#[oai(path = "/user", method = "post")]
async fn create_user(&self, request: req::CreateUser) -> resp::CreateUser {
let namespace = SaneName::new("default".to_string()).expect("hardcoded");
let req::CreateUser::WithName(PlainText(name)) = request;
let Ok(name) = SaneName::new(name) else {
return resp::CreateUser::NameIsTooComplex;
};
match self.storage.create_user(&namespace, &name).await {
Ok(id) => resp::CreateUser::Bearer(PlainText(make_jwt(id, 1.hours(), true))),
Err(err) => {
if err.kind() == std::io::ErrorKind::AlreadyExists {
resp::CreateUser::NameAlreadyTaken
} else {
resp::CreateUser::Other
}
}
}
}
#[oai(path = "/login/prepare", method = "post")]
async fn prepare_login(&self, request: req::PrepareLogin) -> resp::PrepareLogin {
let req::PrepareLogin::WithName(PlainText(name)) = request;
let Ok(name) = SaneName::new(name) else {
return resp::PrepareLogin::NameIsTooComplex;
};
let namespace = SaneName::new("default".to_string()).expect("hardcoded");
let Ok(id) = self.storage.user_id_by_name(&namespace, &name).await else {
return resp::PrepareLogin::UsernameNotFound;
};
let Ok(login_challenge) = self
.storage
.setup_login_challenge(&self.webauthn, &id)
.await
else {
return resp::PrepareLogin::Other;
};
let webauthn = if let Some(webauthn) = login_challenge.webauthn {
if let Ok(value) = serde_json::to_value(webauthn) {
Some(value)
} else {
return resp::PrepareLogin::Other;
}
} else {
None
};
let payload = resp::LoginChallenges {
password: login_challenge.password,
totp: login_challenge.totp,
webauthn,
};
resp::PrepareLogin::Challenges(Json(payload))
}
#[oai(path = "/login/finish", method = "post")]
async fn finish_login(&self, request: req::FinishLogin) -> resp::FinishLogin {
let req::FinishLogin::WithName(Json(req::AuthenticationChallengeResponse {
username,
password,
totp,
pubkey_credential,
existing_jwt,
})) = request;
let Ok(name) = SaneName::new(username) else {
return resp::FinishLogin::WrongUsername;
};
let namespace = SaneName::new("default".to_string()).expect("hardcoded");
let Ok(id) = self.storage.user_id_by_name(&namespace, &name).await else {
return resp::FinishLogin::WrongUsername;
};
let allow_update_creds = if let Some(jwt) = existing_jwt {
let Some(claims) = check_jwt(&jwt) else {
return resp::FinishLogin::Unauthorized;
};
if claims.id != id {
return resp::FinishLogin::Unauthorized;
}
true
} else {
false
};
let pubkey_credential = if let Some(pubkey) = pubkey_credential {
match serde_json::from_value(pubkey) {
Ok(pubkey) => Some(pubkey),
Err(_) => return resp::FinishLogin::InvalidDataFormat,
}
} else {
None
};
let exp = if allow_update_creds { 1 } else { 8 }.hours();
match self
.storage
.verify_login_challenge(
&self.webauthn,
&id,
password.as_deref(),
totp,
pubkey_credential,
)
.await
{
Ok(true) => resp::FinishLogin::Bearer(PlainText(make_jwt(id, exp, allow_update_creds))),
Ok(false) => resp::FinishLogin::Unauthorized,
Err(_) => resp::FinishLogin::Other,
}
}
#[oai(path = "/current_credentials", method = "get")]
pub async fn current_credentials(
&self,
UserToken(token): UserToken,
) -> resp::CurrentCredentials {
let id = token.id;
let Ok(auth_data) = self.storage.load_auth_data(&id).await else {
return resp::CurrentCredentials::Other;
};
resp::CurrentCredentials::List(Json(resp::CurrentCredentialsList {
password: auth_data.phc.is_some(),
totp: auth_data.totp_setup.is_some(),
webauthn_keys: auth_data.webauthn.keys().len(),
}))
}
#[oai(path = "/webauthn", method = "get")]
pub async fn current_webauthn_credentials(
&self,
UserToken(token): UserToken,
) -> resp::CurrentWebauthnCredentials {
let id = token.id;
let Ok(auth_data) = self.storage.load_auth_data(&id).await else {
return resp::CurrentWebauthnCredentials::Other;
};
let keys: Vec<WebAuthnCredential> = auth_data
.webauthn
.keys()
.map(|k| WebAuthnCredential {
name: k.to_string(),
})
.collect();
resp::CurrentWebauthnCredentials::List(Json(keys))
}
#[oai(path = "/password", method = "post")]
pub async fn set_password(
&self,
UserToken(token): UserToken,
request: req::SetPassword,
) -> resp::SimpleTask {
if !token.update_creds {
return resp::SimpleTask::InsufficientPermissions;
}
let id = token.id;
let Ok(mut auth_data) = self.storage.load_auth_data(&id).await else {
return resp::SimpleTask::Failed;
};
let req::SetPassword::NewPassword(PlainText(new_password)) = request;
if auth_data.set_password(&new_password).is_ok()
&& self.storage.save_auth_data(&id, &auth_data).await.is_ok()
{
resp::SimpleTask::Done
} else {
resp::SimpleTask::Failed
}
}
#[oai(path = "/password", method = "delete")]
pub async fn delete_password(&self, UserToken(token): UserToken) -> resp::SimpleTask {
if !token.update_creds {
return resp::SimpleTask::InsufficientPermissions;
}
let id = token.id;
let Ok(mut auth_data) = self.storage.load_auth_data(&id).await else {
return resp::SimpleTask::Failed;
};
auth_data.phc = None;
if self.storage.save_auth_data(&id, &auth_data).await.is_ok() {
resp::SimpleTask::Done
} else {
resp::SimpleTask::Failed
}
}
#[oai(path = "/totp/setup", method = "post")]
pub async fn new_totp(
&self,
UserToken(token): UserToken,
request: req::NewTotp,
) -> resp::NewTotp {
if !token.update_creds {
return resp::NewTotp::InsufficientPermissions;
}
let id = token.id;
let Ok(totp) = self.storage.new_totp(&id).await else {
return resp::NewTotp::Other;
};
let req::NewTotp::WithLabel(Json(req::TotpLabel { label, issuer })) = request;
let setup = resp::NewTotpSetup {
seed_base32: totp.base32_secret(),
authenticator_url: totp.to_uri(&label, &issuer),
};
resp::NewTotp::Setup(Json(setup))
}
#[oai(path = "/totp/confirm", method = "post")]
pub async fn confirm_new_totp(
&self,
UserToken(token): UserToken,
request: req::ConfirmNewTotp,
) -> resp::SimpleTask {
if !token.update_creds {
return resp::SimpleTask::InsufficientPermissions;
}
let id = token.id;
let req::ConfirmNewTotp::CurrentTotp(PlainText(proposed_totp)) = request;
let Ok(proposed_totp) = proposed_totp.parse::<u32>() else {
return resp::SimpleTask::BadInput;
};
match self
.storage
.confirm_new_totp_challenge(&id, proposed_totp)
.await
{
Ok(true) => resp::SimpleTask::Done,
Ok(false) => resp::SimpleTask::BadInput,
_ => resp::SimpleTask::Failed,
}
}
#[oai(path = "/totp", method = "delete")]
pub async fn delete_totp(&self, UserToken(token): UserToken) -> resp::SimpleTask {
if !token.update_creds {
return resp::SimpleTask::InsufficientPermissions;
}
let id = token.id;
let Ok(mut auth_data) = self.storage.load_auth_data(&id).await else {
return resp::SimpleTask::Failed;
};
auth_data.totp_setup = None;
if self.storage.save_auth_data(&id, &auth_data).await.is_ok() {
resp::SimpleTask::Done
} else {
resp::SimpleTask::Failed
}
}
#[oai(path = "/webauthn/register", method = "get")]
pub async fn new_webauthn_credential(
&self,
UserToken(token): UserToken,
) -> resp::NewWebauthnCredential {
if !token.update_creds {
return resp::NewWebauthnCredential::InsufficientPermissions;
}
let id = token.id;
match self
.storage
.new_webauthn_credential(&id, &self.webauthn)
.await
{
Ok(setup) => {
if let Ok(json) = serde_json::to_value(setup) {
resp::NewWebauthnCredential::Setup(Json(json))
} else {
resp::NewWebauthnCredential::Other
}
}
Err(e) => {
eprintln!("{e}");
resp::NewWebauthnCredential::Other
}
}
}
#[oai(path = "/webauthn/register", method = "post")]
pub async fn confirm_new_webauthn_credential(
&self,
UserToken(token): UserToken,
request: req::ConfirmNewWebauthnCredential,
) -> resp::ConfirmNewWebauthnCredential {
if !token.update_creds {
return resp::ConfirmNewWebauthnCredential::InsufficientPermissions;
}
let id = token.id;
let req::ConfirmNewWebauthnCredential::ChallengeResponse(Json(
req::WebauthnRegisterChallengeResponse {
key_name,
register_credential,
},
)) = request;
let Ok(key_name) = SaneName::new(key_name) else {
return resp::ConfirmNewWebauthnCredential::KeyNameIsTooComplex;
};
let Ok(register_credential) = serde_json::from_value(register_credential) else {
return resp::ConfirmNewWebauthnCredential::BadWebauthnPayload;
};
match self
.storage
.confirm_new_webauthn_credential(&id, &self.webauthn, key_name, register_credential)
.await
{
Ok(_) => resp::ConfirmNewWebauthnCredential::Done,
Err(_) => resp::ConfirmNewWebauthnCredential::AuthFailed,
}
}
#[oai(path = "/webauthn/", method = "delete")]
pub async fn delete_webauthn_credential(
&self,
UserToken(token): UserToken,
request: req::DeleteWebAuthnCredential,
) -> resp::SimpleTask {
if !token.update_creds {
return resp::SimpleTask::InsufficientPermissions;
}
let req::DeleteWebAuthnCredential::WithKeyName(PlainText(key_name)) = request;
let Ok(key_name) = SaneName::new(key_name) else {
return resp::SimpleTask::BadInput;
};
match self
.storage
.delete_webauthn_credential(&token.id, key_name)
.await
{
Ok(_) => resp::SimpleTask::Done,
Err(_) => resp::SimpleTask::Failed,
}
}
}