diff options
-rw-r--r-- | kittybox-rs/Cargo.toml | 5 | ||||
-rw-r--r-- | kittybox-rs/src/indieauth/backend.rs | 12 | ||||
-rw-r--r-- | kittybox-rs/src/indieauth/backend/fs.rs | 10 | ||||
-rw-r--r-- | kittybox-rs/src/indieauth/mod.rs | 19 | ||||
-rw-r--r-- | kittybox.nix | 12 |
5 files changed, 48 insertions, 10 deletions
diff --git a/kittybox-rs/Cargo.toml b/kittybox-rs/Cargo.toml index c9b98a2..6b0057f 100644 --- a/kittybox-rs/Cargo.toml +++ b/kittybox-rs/Cargo.toml @@ -7,9 +7,10 @@ default-run = "kittybox" autobins = false [features] -default = ["openssl"] +default = ["rustls"] #util = ["anyhow"] #migration = ["util"] +webauthn = ["openssl", "dep:webauthn"] openssl = ["reqwest/native-tls-vendored", "reqwest/native-tls-alpn"] rustls = ["reqwest/rustls-tls-webpki-roots"] cli = ["clap"] @@ -85,7 +86,7 @@ tracing-tree = "0.2.1" tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } tower-http = { version = "0.3.3", features = ["trace", "cors", "catch-panic"] } tower = { version = "0.4.12", features = ["tracing"] } -webauthn = { version = "0.4.5", package = "webauthn-rs", features = ["danger-allow-state-serialisation"] } +webauthn = { version = "0.4.5", package = "webauthn-rs", features = ["danger-allow-state-serialisation"], optional = true } [dependencies.tokio] version = "^1.16.1" features = ["full", "tracing"] # TODO determine if my app doesn't need some features diff --git a/kittybox-rs/src/indieauth/backend.rs b/kittybox-rs/src/indieauth/backend.rs index 8b0c10a..534bcfb 100644 --- a/kittybox-rs/src/indieauth/backend.rs +++ b/kittybox-rs/src/indieauth/backend.rs @@ -9,7 +9,6 @@ type Result<T> = std::io::Result<T>; pub mod fs; pub use fs::FileBackend; - #[async_trait::async_trait] pub trait AuthBackend: Clone + Send + Sync + 'static { // Authorization code management. @@ -44,7 +43,10 @@ pub trait AuthBackend: Clone + Send + Sync + 'static { /// Enroll a password credential for a user. Only one password /// credential must exist for a given user. async fn enroll_password(&self, website: &url::Url, password: String) -> Result<()>; + /// List currently enrolled credential types for a given user. + async fn list_user_credential_types(&self, website: &url::Url) -> Result<Vec<EnrolledCredential>>; // WebAuthn credential management. + #[cfg(feature = "webauthn")] /// Enroll a WebAuthn authenticator public key for this user. /// Multiple public keys may be saved for one user, corresponding /// to different authenticators used by them. @@ -53,8 +55,10 @@ pub trait AuthBackend: Clone + Send + Sync + 'static { /// updated version after using /// [webauthn::prelude::Passkey::update_credential()]. async fn enroll_webauthn(&self, website: &url::Url, credential: webauthn::prelude::Passkey) -> Result<()>; + #[cfg(feature = "webauthn")] /// List currently enrolled WebAuthn authenticators for a given user. async fn list_webauthn_pubkeys(&self, website: &url::Url) -> Result<Vec<webauthn::prelude::Passkey>>; + #[cfg(feature = "webauthn")] /// Persist registration challenge state for a little while so it /// can be used later. /// @@ -65,6 +69,7 @@ pub trait AuthBackend: Clone + Send + Sync + 'static { website: &url::Url, state: webauthn::prelude::PasskeyRegistration ) -> Result<String>; + #[cfg(feature = "webauthn")] /// Retrieve a persisted registration challenge. /// /// The challenge should be deleted after retrieval. @@ -73,6 +78,7 @@ pub trait AuthBackend: Clone + Send + Sync + 'static { website: &url::Url, challenge_id: &str ) -> Result<webauthn::prelude::PasskeyRegistration>; + #[cfg(feature = "webauthn")] /// Persist authentication challenge state for a little while so /// it can be used later. /// @@ -86,6 +92,7 @@ pub trait AuthBackend: Clone + Send + Sync + 'static { website: &url::Url, state: webauthn::prelude::PasskeyAuthentication ) -> Result<String>; + #[cfg(feature = "webauthn")] /// Retrieve a persisted authentication challenge. /// /// The challenge should be deleted after retrieval. @@ -94,6 +101,5 @@ pub trait AuthBackend: Clone + Send + Sync + 'static { website: &url::Url, challenge_id: &str ) -> Result<webauthn::prelude::PasskeyAuthentication>; - /// List currently enrolled credential types for a given user. - async fn list_user_credential_types(&self, website: &url::Url) -> Result<Vec<EnrolledCredential>>; + } diff --git a/kittybox-rs/src/indieauth/backend/fs.rs b/kittybox-rs/src/indieauth/backend/fs.rs index fbfa0f7..57bc3bd 100644 --- a/kittybox-rs/src/indieauth/backend/fs.rs +++ b/kittybox-rs/src/indieauth/backend/fs.rs @@ -7,6 +7,7 @@ use kittybox_indieauth::{ }; use serde::de::DeserializeOwned; use tokio::{task::spawn_blocking, io::AsyncReadExt}; +#[cfg(feature = "webauthn")] use webauthn::prelude::{Passkey, PasskeyRegistration, PasskeyAuthentication}; const CODE_LENGTH: usize = 16; @@ -343,15 +344,18 @@ impl AuthBackend for FileBackend { } // WebAuthn credential management. + #[cfg(feature = "webauthn")] async fn enroll_webauthn(&self, website: &url::Url, credential: Passkey) -> Result<()> { todo!() } + #[cfg(feature = "webauthn")] async fn list_webauthn_pubkeys(&self, website: &url::Url) -> Result<Vec<Passkey>> { // TODO stub! Ok(vec![]) } + #[cfg(feature = "webauthn")] async fn persist_registration_challenge( &self, website: &url::Url, @@ -360,6 +364,7 @@ impl AuthBackend for FileBackend { todo!() } + #[cfg(feature = "webauthn")] async fn retrieve_registration_challenge( &self, website: &url::Url, @@ -368,6 +373,7 @@ impl AuthBackend for FileBackend { todo!() } + #[cfg(feature = "webauthn")] async fn persist_authentication_challenge( &self, website: &url::Url, @@ -376,6 +382,7 @@ impl AuthBackend for FileBackend { todo!() } + #[cfg(feature = "webauthn")] async fn retrieve_authentication_challenge( &self, website: &url::Url, @@ -392,12 +399,13 @@ impl AuthBackend for FileBackend { .join("password")) .await { - Ok(metadata) => creds.push(EnrolledCredential::Password), + Ok(_) => creds.push(EnrolledCredential::Password), Err(err) => if err.kind() != std::io::ErrorKind::NotFound { return Err(err) } } + #[cfg(feature = "webauthn")] if !self.list_webauthn_pubkeys(website).await?.is_empty() { creds.push(EnrolledCredential::WebAuthn); } diff --git a/kittybox-rs/src/indieauth/mod.rs b/kittybox-rs/src/indieauth/mod.rs index adf669e..67f4a43 100644 --- a/kittybox-rs/src/indieauth/mod.rs +++ b/kittybox-rs/src/indieauth/mod.rs @@ -17,6 +17,7 @@ use kittybox_indieauth::{ }; pub mod backend; +#[cfg(feature = "webauthn")] mod webauthn; use backend::AuthBackend; @@ -111,6 +112,7 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>( #[serde(untagged)] enum Credential { Password(String), + #[cfg(feature = "webauthn")] WebAuthn(::webauthn::prelude::PublicKeyCredential) } @@ -128,6 +130,7 @@ async fn verify_credential<A: AuthBackend>( ) -> std::io::Result<bool> { match credential { Credential::Password(password) => auth.verify_password(website, password).await, + #[cfg(feature = "webauthn")] Credential::WebAuthn(credential) => webauthn::verify( auth, website, @@ -145,8 +148,12 @@ async fn authorization_endpoint_confirm<A: AuthBackend>( cookies: CookieJar, ) -> Response { tracing::debug!("Received authorization confirmation from user"); + #[cfg(feature = "webauthn")] let challenge_id = cookies.get(webauthn::CHALLENGE_ID_COOKIE) .map(|cookie| cookie.value()); + #[cfg(not(feature = "webauthn"))] + let challenge_id = None; + let website = format!("https://{}/", host).parse().unwrap(); let AuthorizationConfirmation { authorization_method: credential, @@ -195,6 +202,7 @@ async fn authorization_endpoint_confirm<A: AuthBackend>( // opaque response instead that is completely useless (StatusCode::NO_CONTENT, [("Location", location.as_str())], + #[cfg(feature = "webauthn")] cookies.remove(Cookie::named(webauthn::CHALLENGE_ID_COOKIE)) ) .into_response() @@ -309,7 +317,7 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>( .unwrap() .as_secs() .into() - } + } } #[inline] @@ -521,7 +529,7 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>( tracing::error!("Error revoking refresh token: {}", err); return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } - + GrantResponse::AccessToken { me: data.me, profile, @@ -695,8 +703,13 @@ pub fn router<A: AuthBackend, D: Storage + 'static>(backend: A, db: D) -> axum:: .route( "/userinfo", get(userinfo_endpoint_get::<A, D>)) + .route("/webauthn/pre_register", - get(webauthn::webauthn_pre_register::<A, D>)) + get( + #[cfg(feature = "webauthn")] webauthn::webauthn_pre_register::<A, D>, + #[cfg(not(feature = "webauthn"))] || async { axum::http::StatusCode::NOT_FOUND } + ) + ) .layer(tower_http::cors::CorsLayer::new() .allow_methods([ axum::http::Method::GET, diff --git a/kittybox.nix b/kittybox.nix index e61843b..397e057 100644 --- a/kittybox.nix +++ b/kittybox.nix @@ -1,4 +1,9 @@ -{ stdenv, lib, openssl, zlib, pkg-config, protobuf, naersk, lld, mold }: +{ stdenv, lib, naersk, lld, mold +, openssl, zlib, pkg-config, protobuf +, useWebAuthn ? false }: + +assert useWebAuthn -> openssl != null && pkg-config != null; + naersk.buildPackage { pname = "kittybox"; version = "0.1.0"; @@ -6,6 +11,11 @@ naersk.buildPackage { src = ./kittybox-rs; doCheck = stdenv.hostPlatform == stdenv.targetPlatform; + cargoOptions = x: x ++ (lib.optionals useWebAuthn [ + "--no-default-features" "--features=\"webauthn\"" + ]); + buildInputs = lib.optional useWebAuthn openssl; + nativeBuildInputs = lib.optional useWebAuthn pkg-config; meta = with lib.meta; { maintainers = with lib.maintainers; [ vikanezrimaya ]; |