use axum::{
extract::{Json, Host},
response::{IntoResponse, Response},
http::StatusCode, Extension
};
use axum_extra::extract::cookie::{CookieJar, Cookie};
use axum_extra::{TypedHeader, headers::{authorization::Bearer, Authorization}};
use super::backend::AuthBackend;
use crate::database::Storage;
pub(crate) const CHALLENGE_ID_COOKIE: &str = "kittybox_webauthn_challenge_id";
macro_rules! bail {
($msg:literal, $err:expr) => {
{
::tracing::error!($msg, $err);
return ::axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
}
}
pub async fn webauthn_pre_register<A: AuthBackend, D: Storage + 'static>(
Host(host): Host,
Extension(db): Extension<D>,
Extension(auth): Extension<A>,
cookies: CookieJar
) -> Response {
let uid = format!("https://{}/", host.clone());
let uid_url: url::Url = uid.parse().unwrap();
// This will not find an h-card in onboarding!
let display_name = match db.get_post(&uid).await {
Ok(hcard) => match hcard {
Some(mut hcard) => {
match hcard["properties"]["uid"][0].take() {
serde_json::Value::String(name) => name,
_ => String::default()
}
},
None => String::default()
},
Err(err) => bail!("Error retrieving h-card: {}", err)
};
let webauthn = webauthn::WebauthnBuilder::new(
&host,
&uid_url
)
.unwrap()
.rp_name("Kittybox")
.build()
.unwrap();
let (challenge, state) = match webauthn.start_passkey_registration(
// Note: using a nil uuid here is fine
// Because the user corresponds to a website anyway
// We do not track multiple users
webauthn::prelude::Uuid::nil(),
&uid,
&display_name,
Some(vec![])
) {
Ok((challenge, state)) => (challenge, state),
Err(err) => bail!("Error generating WebAuthn registration data: {}", err)
};
match auth.persist_registration_challenge(&uid_url, state).await {
Ok(challenge_id) => (
cookies.add(
Cookie::build((CHALLENGE_ID_COOKIE, challenge_id))
.secure(true)
.finish()
),
Json(challenge)
).into_response(),
Err(err) => bail!("Failed to persist WebAuthn challenge: {}", err)
}
}
pub async fn webauthn_register<A: AuthBackend>(
Host(host): Host,
Json(credential): Json<webauthn::prelude::RegisterPublicKeyCredential>,
// TODO determine if we can use a cookie maybe?
user_credential: Option<TypedHeader<Authorization<Bearer>>>,
Extension(auth): Extension<A>
) -> Response {
let uid = format!("https://{}/", host.clone());
let uid_url: url::Url = uid.parse().unwrap();
let pubkeys = match auth.list_webauthn_pubkeys(&uid_url).await {
Ok(pubkeys) => pubkeys,
Err(err) => bail!("Error enumerating existing WebAuthn credentials: {}", err)
};
if !pubkeys.is_empty() {
if let Some(TypedHeader(Authorization(token))) = user_credential {
// TODO check validity of the credential
} else {
return StatusCode::UNAUTHORIZED.into_response()
}
}
return StatusCode::OK.into_response()
}
pub(crate) async fn verify<A: AuthBackend>(
auth: &A,
website: &url::Url,
credential: webauthn::prelude::PublicKeyCredential,
challenge_id: &str
) -> std::io::Result<bool> {
let host = website.host_str().unwrap();
let webauthn = webauthn::WebauthnBuilder::new(
host,
website
)
.unwrap()
.rp_name("Kittybox")
.build()
.unwrap();
match webauthn.finish_passkey_authentication(
&credential,
&auth.retrieve_authentication_challenge(&website, challenge_id).await?
) {
Err(err) => {
tracing::error!("WebAuthn error: {}", err);
Ok(false)
},
Ok(authentication_result) => {
let counter = authentication_result.counter();
let cred_id = authentication_result.cred_id();
if authentication_result.needs_update() {
todo!()
}
Ok(true)
}
}
}