about summary refs log tree commit diff
path: root/kittybox-rs/src/indieauth/webauthn.rs
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-09-19 17:30:38 +0300
committerVika <vika@fireburn.ru>2022-09-19 17:30:38 +0300
commit66049566ae865e1a4bd049257d6afc0abded16e9 (patch)
tree6013a26fa98a149d103eb4402ca91d698ef02ac2 /kittybox-rs/src/indieauth/webauthn.rs
parent696458657b26032e6e2a987c059fd69aaa10508d (diff)
downloadkittybox-66049566ae865e1a4bd049257d6afc0abded16e9.tar.zst
feat: indieauth support
Working:
 - Tokens and codes
 - Authenticating with a password

Not working:
 - Setting the password (need to patch onboarding)
 - WebAuthn (the JavaScript is too complicated)
Diffstat (limited to 'kittybox-rs/src/indieauth/webauthn.rs')
-rw-r--r--kittybox-rs/src/indieauth/webauthn.rs140
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)
+        }
+    }
+}