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( Host(host): Host, Extension(db): Extension, Extension(auth): Extension, 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( Host(host): Host, Json(credential): Json, // TODO determine if we can use a cookie maybe? user_credential: Option>>, Extension(auth): Extension ) -> 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( auth: &A, website: &url::Url, credential: webauthn::prelude::PublicKeyCredential, challenge_id: &str ) -> std::io::Result { 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) } } }