diff options
Diffstat (limited to 'kittybox-rs/src/indieauth/webauthn.rs')
-rw-r--r-- | kittybox-rs/src/indieauth/webauthn.rs | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/kittybox-rs/src/indieauth/webauthn.rs b/kittybox-rs/src/indieauth/webauthn.rs new file mode 100644 index 0000000..ea3ad3d --- /dev/null +++ b/kittybox-rs/src/indieauth/webauthn.rs @@ -0,0 +1,140 @@ +use axum::{ + extract::{Json, Host}, + response::{IntoResponse, Response}, + http::StatusCode, Extension, TypedHeader, headers::{authorization::Bearer, Authorization} +}; +use axum_extra::extract::cookie::{CookieJar, Cookie}; + +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) + } + } +} |