about summary refs log tree commit diff
path: root/src/indieauth
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2025-04-09 23:31:02 +0300
committerVika <vika@fireburn.ru>2025-04-09 23:31:57 +0300
commit8826d9446e6c492db2243b9921e59ce496027bef (patch)
tree63738aa9001cb73b11cb0e974e93129bcdf1adbb /src/indieauth
parent519cadfbb298f50cbf819dde757037ab56e2863e (diff)
downloadkittybox-8826d9446e6c492db2243b9921e59ce496027bef.tar.zst
cargo fmt
Change-Id: I80e81ebba3f0cdf8c094451c9fe3ee4126b8c888
Diffstat (limited to 'src/indieauth')
-rw-r--r--src/indieauth/backend.rs89
-rw-r--r--src/indieauth/backend/fs.rs282
-rw-r--r--src/indieauth/mod.rs727
-rw-r--r--src/indieauth/webauthn.rs76
4 files changed, 713 insertions, 461 deletions
diff --git a/src/indieauth/backend.rs b/src/indieauth/backend.rs
index b913256..9215adf 100644
--- a/src/indieauth/backend.rs
+++ b/src/indieauth/backend.rs
@@ -1,9 +1,7 @@
-use std::future::Future;
-use std::collections::HashMap;
-use kittybox_indieauth::{
-    AuthorizationRequest, TokenData
-};
+use kittybox_indieauth::{AuthorizationRequest, TokenData};
 pub use kittybox_util::auth::EnrolledCredential;
+use std::collections::HashMap;
+use std::future::Future;
 
 type Result<T> = std::io::Result<T>;
 
@@ -20,33 +18,72 @@ pub trait AuthBackend: Clone + Send + Sync + 'static {
     /// Note for implementors: the [`AuthorizationRequest::me`] value
     /// is guaranteed to be [`Some(url::Url)`][Option::Some] and can
     /// be trusted to be correct and non-malicious.
-    fn create_code(&self, data: AuthorizationRequest) -> impl Future<Output = Result<String>> + Send;
+    fn create_code(
+        &self,
+        data: AuthorizationRequest,
+    ) -> impl Future<Output = Result<String>> + Send;
     /// Retreive an authorization request using the one-time
     /// code. Implementations must sanitize the `code` field to
     /// prevent exploits, and must check if the code should still be
     /// valid at this point in time (validity interval is left up to
     /// the implementation, but is recommended to be no more than 10
     /// minutes).
-    fn get_code(&self, code: &str) -> impl Future<Output = Result<Option<AuthorizationRequest>>> + Send;
+    fn get_code(
+        &self,
+        code: &str,
+    ) -> impl Future<Output = Result<Option<AuthorizationRequest>>> + Send;
     // Token management.
     fn create_token(&self, data: TokenData) -> impl Future<Output = Result<String>> + Send;
-    fn get_token(&self, website: &url::Url, token: &str) -> impl Future<Output = Result<Option<TokenData>>> + Send;
-    fn list_tokens(&self, website: &url::Url) -> impl Future<Output = Result<HashMap<String, TokenData>>> + Send;
-    fn revoke_token(&self, website: &url::Url, token: &str) -> impl Future<Output = Result<()>> + Send;
+    fn get_token(
+        &self,
+        website: &url::Url,
+        token: &str,
+    ) -> impl Future<Output = Result<Option<TokenData>>> + Send;
+    fn list_tokens(
+        &self,
+        website: &url::Url,
+    ) -> impl Future<Output = Result<HashMap<String, TokenData>>> + Send;
+    fn revoke_token(
+        &self,
+        website: &url::Url,
+        token: &str,
+    ) -> impl Future<Output = Result<()>> + Send;
     // Refresh token management.
     fn create_refresh_token(&self, data: TokenData) -> impl Future<Output = Result<String>> + Send;
-    fn get_refresh_token(&self, website: &url::Url, token: &str) -> impl Future<Output = Result<Option<TokenData>>> + Send;
-    fn list_refresh_tokens(&self, website: &url::Url) -> impl Future<Output = Result<HashMap<String, TokenData>>> + Send;
-    fn revoke_refresh_token(&self, website: &url::Url, token: &str) -> impl Future<Output = Result<()>> + Send;
+    fn get_refresh_token(
+        &self,
+        website: &url::Url,
+        token: &str,
+    ) -> impl Future<Output = Result<Option<TokenData>>> + Send;
+    fn list_refresh_tokens(
+        &self,
+        website: &url::Url,
+    ) -> impl Future<Output = Result<HashMap<String, TokenData>>> + Send;
+    fn revoke_refresh_token(
+        &self,
+        website: &url::Url,
+        token: &str,
+    ) -> impl Future<Output = Result<()>> + Send;
     // Password management.
     /// Verify a password.
     #[must_use]
-    fn verify_password(&self, website: &url::Url, password: String) -> impl Future<Output = Result<bool>> + Send;
+    fn verify_password(
+        &self,
+        website: &url::Url,
+        password: String,
+    ) -> impl Future<Output = Result<bool>> + Send;
     /// Enroll a password credential for a user. Only one password
     /// credential must exist for a given user.
-    fn enroll_password(&self, website: &url::Url, password: String) -> impl Future<Output = Result<()>> + Send;
+    fn enroll_password(
+        &self,
+        website: &url::Url,
+        password: String,
+    ) -> impl Future<Output = Result<()>> + Send;
     /// List currently enrolled credential types for a given user.
-    fn list_user_credential_types(&self, website: &url::Url) -> impl Future<Output = Result<Vec<EnrolledCredential>>> + Send;
+    fn list_user_credential_types(
+        &self,
+        website: &url::Url,
+    ) -> impl Future<Output = Result<Vec<EnrolledCredential>>> + Send;
     // WebAuthn credential management.
     #[cfg(feature = "webauthn")]
     /// Enroll a WebAuthn authenticator public key for this user.
@@ -56,10 +93,17 @@ pub trait AuthBackend: Clone + Send + Sync + 'static {
     /// This function can also be used to overwrite a passkey with an
     /// updated version after using
     /// [webauthn::prelude::Passkey::update_credential()].
-    fn enroll_webauthn(&self, website: &url::Url, credential: webauthn::prelude::Passkey) -> impl Future<Output = Result<()>> + Send;
+    fn enroll_webauthn(
+        &self,
+        website: &url::Url,
+        credential: webauthn::prelude::Passkey,
+    ) -> impl Future<Output = Result<()>> + Send;
     #[cfg(feature = "webauthn")]
     /// List currently enrolled WebAuthn authenticators for a given user.
-    fn list_webauthn_pubkeys(&self, website: &url::Url) -> impl Future<Output = Result<Vec<webauthn::prelude::Passkey>>> + Send;
+    fn list_webauthn_pubkeys(
+        &self,
+        website: &url::Url,
+    ) -> impl Future<Output = Result<Vec<webauthn::prelude::Passkey>>> + Send;
     #[cfg(feature = "webauthn")]
     /// Persist registration challenge state for a little while so it
     /// can be used later.
@@ -69,7 +113,7 @@ pub trait AuthBackend: Clone + Send + Sync + 'static {
     fn persist_registration_challenge(
         &self,
         website: &url::Url,
-        state: webauthn::prelude::PasskeyRegistration
+        state: webauthn::prelude::PasskeyRegistration,
     ) -> impl Future<Output = Result<String>> + Send;
     #[cfg(feature = "webauthn")]
     /// Retrieve a persisted registration challenge.
@@ -78,7 +122,7 @@ pub trait AuthBackend: Clone + Send + Sync + 'static {
     fn retrieve_registration_challenge(
         &self,
         website: &url::Url,
-        challenge_id: &str
+        challenge_id: &str,
     ) -> impl Future<Output = Result<webauthn::prelude::PasskeyRegistration>> + Send;
     #[cfg(feature = "webauthn")]
     /// Persist authentication challenge state for a little while so
@@ -92,7 +136,7 @@ pub trait AuthBackend: Clone + Send + Sync + 'static {
     fn persist_authentication_challenge(
         &self,
         website: &url::Url,
-        state: webauthn::prelude::PasskeyAuthentication
+        state: webauthn::prelude::PasskeyAuthentication,
     ) -> impl Future<Output = Result<String>> + Send;
     #[cfg(feature = "webauthn")]
     /// Retrieve a persisted authentication challenge.
@@ -101,7 +145,6 @@ pub trait AuthBackend: Clone + Send + Sync + 'static {
     fn retrieve_authentication_challenge(
         &self,
         website: &url::Url,
-        challenge_id: &str
+        challenge_id: &str,
     ) -> impl Future<Output = Result<webauthn::prelude::PasskeyAuthentication>> + Send;
-
 }
diff --git a/src/indieauth/backend/fs.rs b/src/indieauth/backend/fs.rs
index f74fbbc..26466fe 100644
--- a/src/indieauth/backend/fs.rs
+++ b/src/indieauth/backend/fs.rs
@@ -1,13 +1,16 @@
-use std::{path::PathBuf, collections::HashMap, borrow::Cow, time::{SystemTime, Duration}};
-
-use super::{AuthBackend, Result, EnrolledCredential};
-use kittybox_indieauth::{
-    AuthorizationRequest, TokenData
+use std::{
+    borrow::Cow,
+    collections::HashMap,
+    path::PathBuf,
+    time::{Duration, SystemTime},
 };
+
+use super::{AuthBackend, EnrolledCredential, Result};
+use kittybox_indieauth::{AuthorizationRequest, TokenData};
 use serde::de::DeserializeOwned;
-use tokio::{task::spawn_blocking, io::AsyncReadExt};
+use tokio::{io::AsyncReadExt, task::spawn_blocking};
 #[cfg(feature = "webauthn")]
-use webauthn::prelude::{Passkey, PasskeyRegistration, PasskeyAuthentication};
+use webauthn::prelude::{Passkey, PasskeyAuthentication, PasskeyRegistration};
 
 const CODE_LENGTH: usize = 16;
 const TOKEN_LENGTH: usize = 128;
@@ -29,7 +32,8 @@ impl FileBackend {
         } else {
             let mut s = String::with_capacity(filename.len());
 
-            filename.chars()
+            filename
+                .chars()
                 .filter(|c| c.is_alphanumeric())
                 .for_each(|c| s.push(c));
 
@@ -38,41 +42,41 @@ impl FileBackend {
     }
 
     #[inline]
-    async fn serialize_to_file<T: 'static + serde::ser::Serialize + Send, B: Into<Option<&'static str>>>(
+    async fn serialize_to_file<
+        T: 'static + serde::ser::Serialize + Send,
+        B: Into<Option<&'static str>>,
+    >(
         &self,
         dir: &str,
         basename: B,
         length: usize,
-        data: T
+        data: T,
     ) -> Result<String> {
         let basename = basename.into();
         let has_ext = basename.is_some();
-        let (filename, mut file) = kittybox_util::fs::mktemp(
-            self.path.join(dir), basename, length
-        )
+        let (filename, mut file) = kittybox_util::fs::mktemp(self.path.join(dir), basename, length)
             .await
             .map(|(name, file)| (name, file.try_into_std().unwrap()))?;
 
         spawn_blocking(move || serde_json::to_writer(&mut file, &data))
             .await
-            .unwrap_or_else(|e| panic!(
-                "Panic while serializing {}: {}",
-                std::any::type_name::<T>(),
-                e
-            ))
+            .unwrap_or_else(|e| {
+                panic!(
+                    "Panic while serializing {}: {}",
+                    std::any::type_name::<T>(),
+                    e
+                )
+            })
             .map(move |_| {
                 (if has_ext {
-                    filename
-                        .extension()
-
+                    filename.extension()
                 } else {
-                    filename
-                        .file_name()
+                    filename.file_name()
                 })
-                    .unwrap()
-                    .to_str()
-                    .unwrap()
-                    .to_owned()
+                .unwrap()
+                .to_str()
+                .unwrap()
+                .to_owned()
             })
             .map_err(|err| err.into())
     }
@@ -86,17 +90,15 @@ impl FileBackend {
     ) -> Result<Option<(PathBuf, SystemTime, T)>>
     where
         T: serde::de::DeserializeOwned + Send,
-        B: Into<Option<&'static str>>
+        B: Into<Option<&'static str>>,
     {
         let basename = basename.into();
-        let path = self.path
-            .join(dir)
-            .join(format!(
-                "{}{}{}",
-                basename.unwrap_or(""),
-                if basename.is_none() { "" } else { "." },
-                FileBackend::sanitize_for_path(filename)
-            ));
+        let path = self.path.join(dir).join(format!(
+            "{}{}{}",
+            basename.unwrap_or(""),
+            if basename.is_none() { "" } else { "." },
+            FileBackend::sanitize_for_path(filename)
+        ));
 
         let data = match tokio::fs::File::open(&path).await {
             Ok(mut file) => {
@@ -106,13 +108,15 @@ impl FileBackend {
 
                 match serde_json::from_slice::<'_, T>(buf.as_slice()) {
                     Ok(data) => data,
-                    Err(err) => return Err(err.into())
+                    Err(err) => return Err(err.into()),
+                }
+            }
+            Err(err) => {
+                if err.kind() == std::io::ErrorKind::NotFound {
+                    return Ok(None);
+                } else {
+                    return Err(err);
                 }
-            },
-            Err(err) => if err.kind() == std::io::ErrorKind::NotFound {
-                return Ok(None)
-            } else {
-                return Err(err)
             }
         };
 
@@ -125,7 +129,8 @@ impl FileBackend {
     #[tracing::instrument]
     fn url_to_dir(url: &url::Url) -> String {
         let host = url.host_str().unwrap();
-        let port = url.port()
+        let port = url
+            .port()
             .map(|port| Cow::Owned(format!(":{}", port)))
             .unwrap_or(Cow::Borrowed(""));
 
@@ -135,23 +140,26 @@ impl FileBackend {
     async fn list_files<'dir, 'this: 'dir, T: DeserializeOwned + Send>(
         &'this self,
         dir: &'dir str,
-        prefix: &'static str
+        prefix: &'static str,
     ) -> Result<HashMap<String, T>> {
         let dir = self.path.join(dir);
 
         let mut hashmap = HashMap::new();
         let mut readdir = match tokio::fs::read_dir(dir).await {
             Ok(readdir) => readdir,
-            Err(err) => if err.kind() == std::io::ErrorKind::NotFound {
-                // empty hashmap
-                return Ok(hashmap);
-            } else {
-                return Err(err);
+            Err(err) => {
+                if err.kind() == std::io::ErrorKind::NotFound {
+                    // empty hashmap
+                    return Ok(hashmap);
+                } else {
+                    return Err(err);
+                }
             }
         };
         while let Some(entry) = readdir.next_entry().await? {
             // safe to unwrap; filenames are alphanumeric
-            let filename = entry.file_name()
+            let filename = entry
+                .file_name()
                 .into_string()
                 .expect("token filenames should be alphanumeric!");
             if let Some(token) = filename.strip_prefix(&format!("{}.", prefix)) {
@@ -166,16 +174,19 @@ impl FileBackend {
                             Err(err) => {
                                 tracing::error!(
                                     "Error decoding token data from file {}: {}",
-                                    entry.path().display(), err
+                                    entry.path().display(),
+                                    err
                                 );
                                 continue;
                             }
                         };
-                    },
-                    Err(err) => if err.kind() == std::io::ErrorKind::NotFound {
-                        continue
-                    } else {
-                        return Err(err)
+                    }
+                    Err(err) => {
+                        if err.kind() == std::io::ErrorKind::NotFound {
+                            continue;
+                        } else {
+                            return Err(err);
+                        }
                     }
                 }
             }
@@ -194,19 +205,27 @@ impl AuthBackend for FileBackend {
 
             path = base.join(&format!(".{}", path.path())).unwrap();
         }
-        tracing::debug!("Initializing File auth backend: {} -> {}", orig_path, path.path());
+        tracing::debug!(
+            "Initializing File auth backend: {} -> {}",
+            orig_path,
+            path.path()
+        );
         Ok(Self {
-            path: std::path::PathBuf::from(path.path())
+            path: std::path::PathBuf::from(path.path()),
         })
     }
 
     // Authorization code management.
     async fn create_code(&self, data: AuthorizationRequest) -> Result<String> {
-        self.serialize_to_file("codes", None, CODE_LENGTH, data).await
+        self.serialize_to_file("codes", None, CODE_LENGTH, data)
+            .await
     }
 
     async fn get_code(&self, code: &str) -> Result<Option<AuthorizationRequest>> {
-        match self.deserialize_from_file("codes", None, FileBackend::sanitize_for_path(code).as_ref()).await? {
+        match self
+            .deserialize_from_file("codes", None, FileBackend::sanitize_for_path(code).as_ref())
+            .await?
+        {
             Some((path, ctime, data)) => {
                 if let Err(err) = tokio::fs::remove_file(path).await {
                     tracing::error!("Failed to clean up authorization code: {}", err);
@@ -217,23 +236,28 @@ impl AuthBackend for FileBackend {
                 } else {
                     Ok(Some(data))
                 }
-            },
-            None => Ok(None)
+            }
+            None => Ok(None),
         }
     }
 
     // Token management.
     async fn create_token(&self, data: TokenData) -> Result<String> {
         let dir = format!("{}/tokens", FileBackend::url_to_dir(&data.me));
-        self.serialize_to_file(&dir, "access", TOKEN_LENGTH, data).await
+        self.serialize_to_file(&dir, "access", TOKEN_LENGTH, data)
+            .await
     }
 
     async fn get_token(&self, website: &url::Url, token: &str) -> Result<Option<TokenData>> {
         let dir = format!("{}/tokens", FileBackend::url_to_dir(website));
-        match self.deserialize_from_file::<TokenData, _>(
-            &dir, "access",
-            FileBackend::sanitize_for_path(token).as_ref()
-        ).await? {
+        match self
+            .deserialize_from_file::<TokenData, _>(
+                &dir,
+                "access",
+                FileBackend::sanitize_for_path(token).as_ref(),
+            )
+            .await?
+        {
             Some((path, _, token)) => {
                 if token.expired() {
                     if let Err(err) = tokio::fs::remove_file(path).await {
@@ -243,8 +267,8 @@ impl AuthBackend for FileBackend {
                 } else {
                     Ok(Some(token))
                 }
-            },
-            None => Ok(None)
+            }
+            None => Ok(None),
         }
     }
 
@@ -258,25 +282,36 @@ impl AuthBackend for FileBackend {
             self.path
                 .join(FileBackend::url_to_dir(website))
                 .join("tokens")
-                .join(format!("access.{}", FileBackend::sanitize_for_path(token)))
-        ).await {
+                .join(format!("access.{}", FileBackend::sanitize_for_path(token))),
+        )
+        .await
+        {
             Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
-            result => result
+            result => result,
         }
     }
 
     // Refresh token management.
     async fn create_refresh_token(&self, data: TokenData) -> Result<String> {
         let dir = format!("{}/tokens", FileBackend::url_to_dir(&data.me));
-        self.serialize_to_file(&dir, "refresh", TOKEN_LENGTH, data).await
+        self.serialize_to_file(&dir, "refresh", TOKEN_LENGTH, data)
+            .await
     }
 
-    async fn get_refresh_token(&self, website: &url::Url, token: &str) -> Result<Option<TokenData>> {
+    async fn get_refresh_token(
+        &self,
+        website: &url::Url,
+        token: &str,
+    ) -> Result<Option<TokenData>> {
         let dir = format!("{}/tokens", FileBackend::url_to_dir(website));
-        match self.deserialize_from_file::<TokenData, _>(
-            &dir, "refresh",
-            FileBackend::sanitize_for_path(token).as_ref()
-        ).await? {
+        match self
+            .deserialize_from_file::<TokenData, _>(
+                &dir,
+                "refresh",
+                FileBackend::sanitize_for_path(token).as_ref(),
+            )
+            .await?
+        {
             Some((path, _, token)) => {
                 if token.expired() {
                     if let Err(err) = tokio::fs::remove_file(path).await {
@@ -286,8 +321,8 @@ impl AuthBackend for FileBackend {
                 } else {
                     Ok(Some(token))
                 }
-            },
-            None => Ok(None)
+            }
+            None => Ok(None),
         }
     }
 
@@ -301,57 +336,80 @@ impl AuthBackend for FileBackend {
             self.path
                 .join(FileBackend::url_to_dir(website))
                 .join("tokens")
-                .join(format!("refresh.{}", FileBackend::sanitize_for_path(token)))
-        ).await {
+                .join(format!("refresh.{}", FileBackend::sanitize_for_path(token))),
+        )
+        .await
+        {
             Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
-            result => result
+            result => result,
         }
     }
 
     // Password management.
     #[tracing::instrument(skip(password))]
     async fn verify_password(&self, website: &url::Url, password: String) -> Result<bool> {
-        use argon2::{Argon2, password_hash::{PasswordHash, PasswordVerifier}};
+        use argon2::{
+            password_hash::{PasswordHash, PasswordVerifier},
+            Argon2,
+        };
 
-        let password_filename = self.path
+        let password_filename = self
+            .path
             .join(FileBackend::url_to_dir(website))
             .join("password");
 
-        tracing::debug!("Reading password for {} from {}", website, password_filename.display());
+        tracing::debug!(
+            "Reading password for {} from {}",
+            website,
+            password_filename.display()
+        );
 
         match tokio::fs::read_to_string(password_filename).await {
             Ok(password_hash) => {
                 let parsed_hash = {
                     let hash = password_hash.trim();
-                    #[cfg(debug_assertions)] tracing::debug!("Password hash: {}", hash);
-                    PasswordHash::new(hash)
-                        .expect("Password hash should be valid!")
+                    #[cfg(debug_assertions)]
+                    tracing::debug!("Password hash: {}", hash);
+                    PasswordHash::new(hash).expect("Password hash should be valid!")
                 };
-                Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())
-            },
-            Err(err) => if err.kind() == std::io::ErrorKind::NotFound {
-                Ok(false)
-            } else {
-                Err(err)
+                Ok(Argon2::default()
+                    .verify_password(password.as_bytes(), &parsed_hash)
+                    .is_ok())
+            }
+            Err(err) => {
+                if err.kind() == std::io::ErrorKind::NotFound {
+                    Ok(false)
+                } else {
+                    Err(err)
+                }
             }
         }
     }
 
     #[tracing::instrument(skip(password))]
     async fn enroll_password(&self, website: &url::Url, password: String) -> Result<()> {
-        use argon2::{Argon2, password_hash::{rand_core::OsRng, PasswordHasher, SaltString}};
+        use argon2::{
+            password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
+            Argon2,
+        };
 
-        let password_filename = self.path
+        let password_filename = self
+            .path
             .join(FileBackend::url_to_dir(website))
             .join("password");
 
         let salt = SaltString::generate(&mut OsRng);
         let argon2 = Argon2::default();
-        let password_hash = argon2.hash_password(password.as_bytes(), &salt)
+        let password_hash = argon2
+            .hash_password(password.as_bytes(), &salt)
             .expect("Hashing a password should not error out")
             .to_string();
 
-        tracing::debug!("Enrolling password for {} at {}", website, password_filename.display());
+        tracing::debug!(
+            "Enrolling password for {} at {}",
+            website,
+            password_filename.display()
+        );
         tokio::fs::write(password_filename, password_hash.as_bytes()).await
     }
 
@@ -371,7 +429,7 @@ impl AuthBackend for FileBackend {
     async fn persist_registration_challenge(
         &self,
         website: &url::Url,
-        state: PasskeyRegistration
+        state: PasskeyRegistration,
     ) -> Result<String> {
         todo!()
     }
@@ -380,7 +438,7 @@ impl AuthBackend for FileBackend {
     async fn retrieve_registration_challenge(
         &self,
         website: &url::Url,
-        challenge_id: &str
+        challenge_id: &str,
     ) -> Result<PasskeyRegistration> {
         todo!()
     }
@@ -389,7 +447,7 @@ impl AuthBackend for FileBackend {
     async fn persist_authentication_challenge(
         &self,
         website: &url::Url,
-        state: PasskeyAuthentication
+        state: PasskeyAuthentication,
     ) -> Result<String> {
         todo!()
     }
@@ -398,24 +456,28 @@ impl AuthBackend for FileBackend {
     async fn retrieve_authentication_challenge(
         &self,
         website: &url::Url,
-        challenge_id: &str
+        challenge_id: &str,
     ) -> Result<PasskeyAuthentication> {
         todo!()
     }
 
     #[tracing::instrument(skip(self))]
-    async fn list_user_credential_types(&self, website: &url::Url) -> Result<Vec<EnrolledCredential>> {
+    async fn list_user_credential_types(
+        &self,
+        website: &url::Url,
+    ) -> Result<Vec<EnrolledCredential>> {
         let mut creds = vec![];
-        let password_file = self.path
+        let password_file = self
+            .path
             .join(FileBackend::url_to_dir(website))
             .join("password");
         tracing::debug!("Password file for {}: {}", website, password_file.display());
-        match tokio::fs::metadata(password_file)
-            .await
-        {
+        match tokio::fs::metadata(password_file).await {
             Ok(_) => creds.push(EnrolledCredential::Password),
-            Err(err) => if err.kind() != std::io::ErrorKind::NotFound {
-                return Err(err)
+            Err(err) => {
+                if err.kind() != std::io::ErrorKind::NotFound {
+                    return Err(err);
+                }
             }
         }
 
diff --git a/src/indieauth/mod.rs b/src/indieauth/mod.rs
index 00ae393..2f90a19 100644
--- a/src/indieauth/mod.rs
+++ b/src/indieauth/mod.rs
@@ -1,18 +1,29 @@
-use std::marker::PhantomData;
-use microformats::types::Class;
-use tracing::error;
-use serde::Deserialize;
+use crate::database::Storage;
 use axum::{
-    extract::{Form, FromRef, Json, Query, State}, http::StatusCode, response::{Html, IntoResponse, Response}
+    extract::{Form, FromRef, Json, Query, State},
+    http::StatusCode,
+    response::{Html, IntoResponse, Response},
 };
 #[cfg_attr(not(feature = "webauthn"), allow(unused_imports))]
-use axum_extra::extract::{Host, cookie::{CookieJar, Cookie}};
-use axum_extra::{headers::{authorization::Bearer, Authorization, ContentType, HeaderMapExt}, TypedHeader};
-use crate::database::Storage;
+use axum_extra::extract::{
+    cookie::{Cookie, CookieJar},
+    Host,
+};
+use axum_extra::{
+    headers::{authorization::Bearer, Authorization, ContentType, HeaderMapExt},
+    TypedHeader,
+};
 use kittybox_indieauth::{
-    AuthorizationRequest, AuthorizationResponse, ClientMetadata, Error, ErrorKind, GrantRequest, GrantResponse, GrantType, IntrospectionEndpointAuthMethod, Metadata, PKCEMethod, Profile, ProfileUrl, ResponseType, RevocationEndpointAuthMethod, Scope, Scopes, TokenData, TokenIntrospectionRequest, TokenIntrospectionResponse, TokenRevocationRequest
+    AuthorizationRequest, AuthorizationResponse, ClientMetadata, Error, ErrorKind, GrantRequest,
+    GrantResponse, GrantType, IntrospectionEndpointAuthMethod, Metadata, PKCEMethod, Profile,
+    ProfileUrl, ResponseType, RevocationEndpointAuthMethod, Scope, Scopes, TokenData,
+    TokenIntrospectionRequest, TokenIntrospectionResponse, TokenRevocationRequest,
 };
+use microformats::types::Class;
+use serde::Deserialize;
+use std::marker::PhantomData;
 use std::str::FromStr;
+use tracing::error;
 
 pub mod backend;
 #[cfg(feature = "webauthn")]
@@ -41,35 +52,42 @@ impl<A: AuthBackend> std::ops::Deref for User<A> {
 pub enum IndieAuthResourceError {
     InvalidRequest,
     Unauthorized,
-    InvalidToken
+    InvalidToken,
 }
 impl axum::response::IntoResponse for IndieAuthResourceError {
     fn into_response(self) -> axum::response::Response {
         use IndieAuthResourceError::*;
 
         match self {
-            Unauthorized => (
-                StatusCode::UNAUTHORIZED,
-                [("WWW-Authenticate", "Bearer")]
-            ).into_response(),
+            Unauthorized => {
+                (StatusCode::UNAUTHORIZED, [("WWW-Authenticate", "Bearer")]).into_response()
+            }
             InvalidRequest => (
                 StatusCode::BAD_REQUEST,
-                Json(&serde_json::json!({"error": "invalid_request"}))
-            ).into_response(),
+                Json(&serde_json::json!({"error": "invalid_request"})),
+            )
+                .into_response(),
             InvalidToken => (
                 StatusCode::UNAUTHORIZED,
                 [("WWW-Authenticate", "Bearer, error=\"invalid_token\"")],
-                Json(&serde_json::json!({"error": "not_authorized"}))
-            ).into_response()
+                Json(&serde_json::json!({"error": "not_authorized"})),
+            )
+                .into_response(),
         }
     }
 }
 
-impl <A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static> axum::extract::OptionalFromRequestParts<St> for User<A> {
+impl<A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static>
+    axum::extract::OptionalFromRequestParts<St> for User<A>
+{
     type Rejection = <Self as axum::extract::FromRequestParts<St>>::Rejection;
 
-    async fn from_request_parts(req: &mut axum::http::request::Parts, state: &St) -> Result<Option<Self>, Self::Rejection> {
-        let res = <Self as axum::extract::FromRequestParts<St>>::from_request_parts(req, state).await;
+    async fn from_request_parts(
+        req: &mut axum::http::request::Parts,
+        state: &St,
+    ) -> Result<Option<Self>, Self::Rejection> {
+        let res =
+            <Self as axum::extract::FromRequestParts<St>>::from_request_parts(req, state).await;
 
         match res {
             Ok(user) => Ok(Some(user)),
@@ -79,14 +97,19 @@ impl <A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static> axum::ext
     }
 }
 
-impl <A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static> axum::extract::FromRequestParts<St> for User<A> {
+impl<A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static>
+    axum::extract::FromRequestParts<St> for User<A>
+{
     type Rejection = IndieAuthResourceError;
 
-    async fn from_request_parts(req: &mut axum::http::request::Parts, state: &St) -> Result<Self, Self::Rejection> {
+    async fn from_request_parts(
+        req: &mut axum::http::request::Parts,
+        state: &St,
+    ) -> Result<Self, Self::Rejection> {
         let TypedHeader(Authorization(token)) =
             TypedHeader::<Authorization<Bearer>>::from_request_parts(req, state)
-            .await
-            .map_err(|_| IndieAuthResourceError::Unauthorized)?;
+                .await
+                .map_err(|_| IndieAuthResourceError::Unauthorized)?;
 
         let auth = A::from_ref(state);
 
@@ -94,10 +117,7 @@ impl <A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static> axum::ext
             .await
             .map_err(|_| IndieAuthResourceError::InvalidRequest)?;
 
-        auth.get_token(
-            &format!("https://{host}/").parse().unwrap(),
-            token.token()
-        )
+        auth.get_token(&format!("https://{host}/").parse().unwrap(), token.token())
             .await
             .unwrap()
             .ok_or(IndieAuthResourceError::InvalidToken)
@@ -105,9 +125,7 @@ impl <A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static> axum::ext
     }
 }
 
-pub async fn metadata(
-    Host(host): Host
-) -> Metadata {
+pub async fn metadata(Host(host): Host) -> Metadata {
     let issuer: url::Url = format!("https://{}/", host).parse().unwrap();
 
     let indieauth: url::Url = issuer.join("/.kittybox/indieauth/").unwrap();
@@ -117,18 +135,16 @@ pub async fn metadata(
         token_endpoint: indieauth.join("token").unwrap(),
         introspection_endpoint: indieauth.join("token_status").unwrap(),
         introspection_endpoint_auth_methods_supported: Some(vec![
-            IntrospectionEndpointAuthMethod::Bearer
+            IntrospectionEndpointAuthMethod::Bearer,
         ]),
         revocation_endpoint: Some(indieauth.join("revoke_token").unwrap()),
-        revocation_endpoint_auth_methods_supported: Some(vec![
-            RevocationEndpointAuthMethod::None
-        ]),
+        revocation_endpoint_auth_methods_supported: Some(vec![RevocationEndpointAuthMethod::None]),
         scopes_supported: Some(vec![
             Scope::Create,
             Scope::Update,
             Scope::Delete,
             Scope::Media,
-            Scope::Profile
+            Scope::Profile,
         ]),
         response_types_supported: Some(vec![ResponseType::Code]),
         grant_types_supported: Some(vec![GrantType::AuthorizationCode, GrantType::RefreshToken]),
@@ -145,27 +161,39 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
     Query(request): Query<AuthorizationRequest>,
     State(db): State<D>,
     State(http): State<reqwest_middleware::ClientWithMiddleware>,
-    State(auth): State<A>
+    State(auth): State<A>,
 ) -> Response {
     let me: url::Url = format!("https://{host}/").parse().unwrap();
     // XXX: attempt fetching OAuth application metadata
-    let h_app: ClientMetadata = if request.client_id.domain().unwrap() == "localhost" && me.domain().unwrap() != "localhost" {
+    let h_app: ClientMetadata = if request.client_id.domain().unwrap() == "localhost"
+        && me.domain().unwrap() != "localhost"
+    {
         // If client is localhost, but we aren't localhost, generate synthetic metadata.
         tracing::warn!("Client is localhost, not fetching metadata");
-        let mut metadata = ClientMetadata::new(request.client_id.clone(), request.client_id.clone()).unwrap();
+        let mut metadata =
+            ClientMetadata::new(request.client_id.clone(), request.client_id.clone()).unwrap();
 
         metadata.client_name = Some("Your locally hosted app".to_string());
 
         metadata
     } else {
         tracing::debug!("Sending request to {} to fetch metadata", request.client_id);
-        let metadata_request = http.get(request.client_id.clone())
+        let metadata_request = http
+            .get(request.client_id.clone())
             .header("Accept", "application/json, text/html");
-        match metadata_request.send().await
-            .and_then(|res| res.error_for_status()
-                .map_err(reqwest_middleware::Error::Reqwest))
-        {
-            Ok(response) if response.headers().typed_get::<ContentType>().to_owned().map(mime::Mime::from).map(|m| m.type_() == "text" && m.subtype() == "html").unwrap_or(false) => {
+        match metadata_request.send().await.and_then(|res| {
+            res.error_for_status()
+                .map_err(reqwest_middleware::Error::Reqwest)
+        }) {
+            Ok(response)
+                if response
+                    .headers()
+                    .typed_get::<ContentType>()
+                    .to_owned()
+                    .map(mime::Mime::from)
+                    .map(|m| m.type_() == "text" && m.subtype() == "html")
+                    .unwrap_or(false) =>
+            {
                 let url = response.url().clone();
                 let text = response.text().await.unwrap();
                 tracing::debug!("Received {} bytes in response", text.len());
@@ -173,76 +201,95 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
                     Ok(mf2) => {
                         if let Some(relation) = mf2.rels.items.get(&request.redirect_uri) {
                             if !relation.rels.iter().any(|i| i == "redirect_uri") {
-                                return (StatusCode::BAD_REQUEST,
-                                        [("Content-Type", "text/plain")],
-                                        "The redirect_uri provided was declared as \
-                                         something other than redirect_uri.")
-                                    .into_response()
+                                return (
+                                    StatusCode::BAD_REQUEST,
+                                    [("Content-Type", "text/plain")],
+                                    "The redirect_uri provided was declared as \
+                                         something other than redirect_uri.",
+                                )
+                                    .into_response();
                             }
                         } else if request.redirect_uri.origin() != request.client_id.origin() {
-                            return (StatusCode::BAD_REQUEST,
-                                    [("Content-Type", "text/plain")],
-                                    "The redirect_uri didn't match the origin \
-                                     and wasn't explicitly allowed. You were being tricked.")
-                                .into_response()
+                            return (
+                                StatusCode::BAD_REQUEST,
+                                [("Content-Type", "text/plain")],
+                                "The redirect_uri didn't match the origin \
+                                     and wasn't explicitly allowed. You were being tricked.",
+                            )
+                                .into_response();
                         }
 
-                        if let Some(app) = mf2.items
+                        if let Some(app) = mf2
+                            .items
                             .iter()
-                            .find(|&i| i.r#type.iter()
-                                .any(|i| {
+                            .find(|&i| {
+                                i.r#type.iter().any(|i| {
                                     *i == Class::from_str("h-app").unwrap()
                                         || *i == Class::from_str("h-x-app").unwrap()
                                 })
-                            )
+                            })
                             .cloned()
                         {
                             // Create a synthetic metadata document. Be forgiving.
                             let mut metadata = ClientMetadata::new(
                                 request.client_id.clone(),
-                                app.properties.get("url")
+                                app.properties
+                                    .get("url")
                                     .and_then(|v| v.first())
                                     .and_then(|i| match i {
-                                        microformats::types::PropertyValue::Url(url) => Some(url.clone()),
-                                        _ => None
+                                        microformats::types::PropertyValue::Url(url) => {
+                                            Some(url.clone())
+                                        }
+                                        _ => None,
                                     })
-                                    .unwrap_or_else(|| request.client_id.clone())
-                            ).unwrap();
+                                    .unwrap_or_else(|| request.client_id.clone()),
+                            )
+                            .unwrap();
 
-                            metadata.client_name = app.properties.get("name")
+                            metadata.client_name = app
+                                .properties
+                                .get("name")
                                 .and_then(|v| v.first())
                                 .and_then(|i| match i {
-                                    microformats::types::PropertyValue::Plain(name) => Some(name.to_owned()),
-                                    _ => None
+                                    microformats::types::PropertyValue::Plain(name) => {
+                                        Some(name.to_owned())
+                                    }
+                                    _ => None,
                                 });
 
                             metadata.redirect_uris = mf2.rels.by_rels().remove("redirect_uri");
 
                             metadata
                         } else {
-                            return (StatusCode::BAD_REQUEST, [("Content-Type", "text/plain")], "No h-app or JSON application metadata found.").into_response()
+                            return (
+                                StatusCode::BAD_REQUEST,
+                                [("Content-Type", "text/plain")],
+                                "No h-app or JSON application metadata found.",
+                            )
+                                .into_response();
                         }
-                    },
+                    }
                     Err(err) => {
                         tracing::error!("Error parsing application metadata: {}", err);
                         return (
                             StatusCode::BAD_REQUEST,
                             [("Content-Type", "text/plain")],
-                            "Parsing h-app metadata failed.").into_response()
+                            "Parsing h-app metadata failed.",
+                        )
+                            .into_response();
                     }
                 }
-            },
+            }
             Ok(response) => match response.json::<ClientMetadata>().await {
-                Ok(client_metadata) => {
-                    client_metadata
-                },
+                Ok(client_metadata) => client_metadata,
                 Err(err) => {
                     tracing::error!("Error parsing JSON application metadata: {}", err);
                     return (
                         StatusCode::BAD_REQUEST,
                         [("Content-Type", "text/plain")],
-                        format!("Parsing OAuth2 JSON app metadata failed: {}", err)
-                    ).into_response()
+                        format!("Parsing OAuth2 JSON app metadata failed: {}", err),
+                    )
+                        .into_response();
                 }
             },
             Err(err) => {
@@ -250,27 +297,32 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
                 return (
                     StatusCode::BAD_REQUEST,
                     [("Content-Type", "text/plain")],
-                    format!("Fetching app metadata failed: {}", err)
-                ).into_response()
+                    format!("Fetching app metadata failed: {}", err),
+                )
+                    .into_response();
             }
         }
     };
 
     tracing::debug!("Application metadata: {:#?}", h_app);
 
-    Html(kittybox_frontend_renderer::Template {
-        title: "Confirm sign-in via IndieAuth",
-        blog_name: "Kittybox",
-        feeds: vec![],
-        user: None,
-        content: kittybox_frontend_renderer::AuthorizationRequestPage {
-            request,
-            credentials: auth.list_user_credential_types(&me).await.unwrap(),
-            user: db.get_post(me.as_str()).await.unwrap().unwrap(),
-            app: h_app
-        }.to_string(),
-    }.to_string())
-        .into_response()
+    Html(
+        kittybox_frontend_renderer::Template {
+            title: "Confirm sign-in via IndieAuth",
+            blog_name: "Kittybox",
+            feeds: vec![],
+            user: None,
+            content: kittybox_frontend_renderer::AuthorizationRequestPage {
+                request,
+                credentials: auth.list_user_credential_types(&me).await.unwrap(),
+                user: db.get_post(me.as_str()).await.unwrap().unwrap(),
+                app: h_app,
+            }
+            .to_string(),
+        }
+        .to_string(),
+    )
+    .into_response()
 }
 
 #[derive(Deserialize, Debug)]
@@ -278,7 +330,7 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
 enum Credential {
     Password(String),
     #[cfg(feature = "webauthn")]
-    WebAuthn(::webauthn::prelude::PublicKeyCredential)
+    WebAuthn(::webauthn::prelude::PublicKeyCredential),
 }
 
 // The IndieAuth standard doesn't prescribe a format for confirming
@@ -291,7 +343,7 @@ enum Credential {
 #[derive(Deserialize, Debug)]
 struct AuthorizationConfirmation {
     authorization_method: Credential,
-    request: AuthorizationRequest
+    request: AuthorizationRequest,
 }
 
 #[tracing::instrument(skip(auth, credential))]
@@ -299,18 +351,14 @@ async fn verify_credential<A: AuthBackend>(
     auth: &A,
     website: &url::Url,
     credential: Credential,
-    #[cfg_attr(not(feature = "webauthn"), allow(unused_variables))]
-    challenge_id: Option<&str>
+    #[cfg_attr(not(feature = "webauthn"), allow(unused_variables))] challenge_id: Option<&str>,
 ) -> 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,
-            credential,
-            challenge_id.unwrap()
-        ).await
+        Credential::WebAuthn(credential) => {
+            webauthn::verify(auth, website, credential, challenge_id.unwrap()).await
+        }
     }
 }
 
@@ -323,7 +371,8 @@ async fn authorization_endpoint_confirm<A: AuthBackend>(
 ) -> Response {
     tracing::debug!("Received authorization confirmation from user");
     #[cfg(feature = "webauthn")]
-    let challenge_id = cookies.get(webauthn::CHALLENGE_ID_COOKIE)
+    let challenge_id = cookies
+        .get(webauthn::CHALLENGE_ID_COOKIE)
         .map(|cookie| cookie.value());
     #[cfg(not(feature = "webauthn"))]
     let challenge_id = None;
@@ -331,14 +380,16 @@ async fn authorization_endpoint_confirm<A: AuthBackend>(
     let website = format!("https://{}/", host).parse().unwrap();
     let AuthorizationConfirmation {
         authorization_method: credential,
-        request: mut auth
+        request: mut auth,
     } = confirmation;
 
     match verify_credential(&backend, &website, credential, challenge_id).await {
-        Ok(verified) => if !verified {
-            error!("User failed verification, bailing out.");
-            return StatusCode::UNAUTHORIZED.into_response();
-        },
+        Ok(verified) => {
+            if !verified {
+                error!("User failed verification, bailing out.");
+                return StatusCode::UNAUTHORIZED.into_response();
+            }
+        }
         Err(err) => {
             error!("Error while verifying credential: {}", err);
             return StatusCode::INTERNAL_SERVER_ERROR.into_response();
@@ -365,9 +416,14 @@ async fn authorization_endpoint_confirm<A: AuthBackend>(
 
     let location = {
         let mut uri = redirect_uri;
-        uri.set_query(Some(&serde_urlencoded::to_string(
-            AuthorizationResponse { code, state, iss: website }
-        ).unwrap()));
+        uri.set_query(Some(
+            &serde_urlencoded::to_string(AuthorizationResponse {
+                code,
+                state,
+                iss: website,
+            })
+            .unwrap(),
+        ));
 
         uri
     };
@@ -375,10 +431,11 @@ async fn authorization_endpoint_confirm<A: AuthBackend>(
     // DO NOT SET `StatusCode::FOUND` here! `fetch()` cannot read from
     // redirects, it can only follow them or choose to receive an
     // opaque response instead that is completely useless
-    (StatusCode::NO_CONTENT,
-     [("Location", location.as_str())],
-     #[cfg(feature = "webauthn")]
-     cookies.remove(Cookie::from(webauthn::CHALLENGE_ID_COOKIE))
+    (
+        StatusCode::NO_CONTENT,
+        [("Location", location.as_str())],
+        #[cfg(feature = "webauthn")]
+        cookies.remove(Cookie::from(webauthn::CHALLENGE_ID_COOKIE)),
     )
         .into_response()
 }
@@ -396,15 +453,18 @@ async fn authorization_endpoint_post<A: AuthBackend, D: Storage + 'static>(
             code,
             client_id,
             redirect_uri,
-            code_verifier
+            code_verifier,
         } => {
             let request: AuthorizationRequest = match backend.get_code(&code).await {
                 Ok(Some(request)) => request,
-                Ok(None) => return Error {
-                    kind: ErrorKind::InvalidGrant,
-                    msg: Some("The provided authorization code is invalid.".to_string()),
-                    error_uri: None
-                }.into_response(),
+                Ok(None) => {
+                    return Error {
+                        kind: ErrorKind::InvalidGrant,
+                        msg: Some("The provided authorization code is invalid.".to_string()),
+                        error_uri: None,
+                    }
+                    .into_response()
+                }
                 Err(err) => {
                     tracing::error!("Error retrieving auth request: {}", err);
                     return StatusCode::INTERNAL_SERVER_ERROR.into_response();
@@ -414,51 +474,66 @@ async fn authorization_endpoint_post<A: AuthBackend, D: Storage + 'static>(
                 return Error {
                     kind: ErrorKind::InvalidGrant,
                     msg: Some("This authorization code isn't yours.".to_string()),
-                    error_uri: None
-                }.into_response()
+                    error_uri: None,
+                }
+                .into_response();
             }
             if redirect_uri != request.redirect_uri {
                 return Error {
                     kind: ErrorKind::InvalidGrant,
-                    msg: Some("This redirect_uri doesn't match the one the code has been sent to.".to_string()),
-                    error_uri: None
-                }.into_response()
+                    msg: Some(
+                        "This redirect_uri doesn't match the one the code has been sent to."
+                            .to_string(),
+                    ),
+                    error_uri: None,
+                }
+                .into_response();
             }
             if !request.code_challenge.verify(code_verifier) {
                 return Error {
                     kind: ErrorKind::InvalidGrant,
                     msg: Some("The PKCE challenge failed.".to_string()),
                     // are RFCs considered human-readable? 😝
-                    error_uri: "https://datatracker.ietf.org/doc/html/rfc7636#section-4.6".parse().ok()
-                }.into_response()
+                    error_uri: "https://datatracker.ietf.org/doc/html/rfc7636#section-4.6"
+                        .parse()
+                        .ok(),
+                }
+                .into_response();
             }
             let me: url::Url = format!("https://{}/", host).parse().unwrap();
             if request.me.unwrap() != me {
                 return Error {
                     kind: ErrorKind::InvalidGrant,
                     msg: Some("This authorization endpoint does not serve this user.".to_string()),
-                    error_uri: None
-                }.into_response()
+                    error_uri: None,
+                }
+                .into_response();
             }
-            let profile = if request.scope.as_ref()
-                                  .map(|s| s.has(&Scope::Profile))
-                                  .unwrap_or_default()
+            let profile = if request
+                .scope
+                .as_ref()
+                .map(|s| s.has(&Scope::Profile))
+                .unwrap_or_default()
             {
                 match get_profile(
                     db,
                     me.as_str(),
-                    request.scope.as_ref()
+                    request
+                        .scope
+                        .as_ref()
                         .map(|s| s.has(&Scope::Email))
-                        .unwrap_or_default()
-                ).await {
+                        .unwrap_or_default(),
+                )
+                .await
+                {
                     Ok(profile) => {
                         tracing::debug!("Retrieved profile: {:?}", profile);
                         profile
-                    },
+                    }
                     Err(err) => {
                         tracing::error!("Error retrieving profile from database: {}", err);
 
-                        return StatusCode::INTERNAL_SERVER_ERROR.into_response()
+                        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                     }
                 }
             } else {
@@ -466,12 +541,15 @@ async fn authorization_endpoint_post<A: AuthBackend, D: Storage + 'static>(
             };
 
             GrantResponse::ProfileUrl(ProfileUrl { me, profile }).into_response()
-        },
+        }
         _ => Error {
             kind: ErrorKind::InvalidGrant,
             msg: Some("The provided grant_type is unusable on this endpoint.".to_string()),
-            error_uri: "https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code".parse().ok()
-        }.into_response()
+            error_uri: "https://indieauth.spec.indieweb.org/#redeeming-the-authorization-code"
+                .parse()
+                .ok(),
+        }
+        .into_response(),
     }
 }
 
@@ -485,36 +563,40 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
     #[inline]
     fn prepare_access_token(me: url::Url, client_id: url::Url, scope: Scopes) -> TokenData {
         TokenData {
-            me, client_id, scope,
+            me,
+            client_id,
+            scope,
             exp: (std::time::SystemTime::now()
-                  .duration_since(std::time::UNIX_EPOCH)
-                  .unwrap()
-                  + std::time::Duration::from_secs(ACCESS_TOKEN_VALIDITY))
-                .as_secs()
-                .into(),
+                .duration_since(std::time::UNIX_EPOCH)
+                .unwrap()
+                + std::time::Duration::from_secs(ACCESS_TOKEN_VALIDITY))
+            .as_secs()
+            .into(),
             iat: std::time::SystemTime::now()
                 .duration_since(std::time::UNIX_EPOCH)
                 .unwrap()
                 .as_secs()
-                .into()
+                .into(),
         }
     }
 
     #[inline]
     fn prepare_refresh_token(me: url::Url, client_id: url::Url, scope: Scopes) -> TokenData {
         TokenData {
-            me, client_id, scope,
+            me,
+            client_id,
+            scope,
             exp: (std::time::SystemTime::now()
-                  .duration_since(std::time::UNIX_EPOCH)
-                  .unwrap()
-                  + std::time::Duration::from_secs(REFRESH_TOKEN_VALIDITY))
-                .as_secs()
-                .into(),
+                .duration_since(std::time::UNIX_EPOCH)
+                .unwrap()
+                + std::time::Duration::from_secs(REFRESH_TOKEN_VALIDITY))
+            .as_secs()
+            .into(),
             iat: std::time::SystemTime::now()
                 .duration_since(std::time::UNIX_EPOCH)
                 .unwrap()
                 .as_secs()
-                .into()
+                .into(),
         }
     }
 
@@ -525,15 +607,18 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
             code,
             client_id,
             redirect_uri,
-            code_verifier
+            code_verifier,
         } => {
             let request: AuthorizationRequest = match backend.get_code(&code).await {
                 Ok(Some(request)) => request,
-                Ok(None) => return Error {
-                    kind: ErrorKind::InvalidGrant,
-                    msg: Some("The provided authorization code is invalid.".to_string()),
-                    error_uri: None
-                }.into_response(),
+                Ok(None) => {
+                    return Error {
+                        kind: ErrorKind::InvalidGrant,
+                        msg: Some("The provided authorization code is invalid.".to_string()),
+                        error_uri: None,
+                    }
+                    .into_response()
+                }
                 Err(err) => {
                     tracing::error!("Error retrieving auth request: {}", err);
                     return StatusCode::INTERNAL_SERVER_ERROR.into_response();
@@ -542,33 +627,46 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
 
             tracing::debug!("Retrieved authorization request: {:?}", request);
 
-            let scope = if let Some(scope) = request.scope { scope } else {
+            let scope = if let Some(scope) = request.scope {
+                scope
+            } else {
                 return Error {
                     kind: ErrorKind::InvalidScope,
                     msg: Some("Tokens cannot be issued if no scopes are requested.".to_string()),
-                    error_uri: "https://indieauth.spec.indieweb.org/#access-token-response".parse().ok()
-                }.into_response();
+                    error_uri: "https://indieauth.spec.indieweb.org/#access-token-response"
+                        .parse()
+                        .ok(),
+                }
+                .into_response();
             };
             if client_id != request.client_id {
                 return Error {
                     kind: ErrorKind::InvalidGrant,
                     msg: Some("This authorization code isn't yours.".to_string()),
-                    error_uri: None
-                }.into_response()
+                    error_uri: None,
+                }
+                .into_response();
             }
             if redirect_uri != request.redirect_uri {
                 return Error {
                     kind: ErrorKind::InvalidGrant,
-                    msg: Some("This redirect_uri doesn't match the one the code has been sent to.".to_string()),
-                    error_uri: None
-                }.into_response()
+                    msg: Some(
+                        "This redirect_uri doesn't match the one the code has been sent to."
+                            .to_string(),
+                    ),
+                    error_uri: None,
+                }
+                .into_response();
             }
             if !request.code_challenge.verify(code_verifier) {
                 return Error {
                     kind: ErrorKind::InvalidGrant,
                     msg: Some("The PKCE challenge failed.".to_string()),
-                    error_uri: "https://datatracker.ietf.org/doc/html/rfc7636#section-4.6".parse().ok()
-                }.into_response();
+                    error_uri: "https://datatracker.ietf.org/doc/html/rfc7636#section-4.6"
+                        .parse()
+                        .ok(),
+                }
+                .into_response();
             }
 
             // Note: we can trust the `request.me` value, since we set
@@ -577,30 +675,32 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
                 return Error {
                     kind: ErrorKind::InvalidGrant,
                     msg: Some("This authorization endpoint does not serve this user.".to_string()),
-                    error_uri: None
-                }.into_response()
+                    error_uri: None,
+                }
+                .into_response();
             }
 
             let profile = if dbg!(scope.has(&Scope::Profile)) {
-                match get_profile(
-                    db,
-                    me.as_str(),
-                    scope.has(&Scope::Email)
-                ).await {
+                match get_profile(db, me.as_str(), scope.has(&Scope::Email)).await {
                     Ok(profile) => dbg!(profile),
                     Err(err) => {
                         tracing::error!("Error retrieving profile from database: {}", err);
 
-                        return StatusCode::INTERNAL_SERVER_ERROR.into_response()
+                        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                     }
                 }
             } else {
                 None
             };
 
-            let access_token = match backend.create_token(
-                prepare_access_token(me.clone(), client_id.clone(), scope.clone())
-            ).await {
+            let access_token = match backend
+                .create_token(prepare_access_token(
+                    me.clone(),
+                    client_id.clone(),
+                    scope.clone(),
+                ))
+                .await
+            {
                 Ok(token) => token,
                 Err(err) => {
                     tracing::error!("Error creating access token: {}", err);
@@ -608,9 +708,10 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
                 }
             };
             // TODO: only create refresh token if user allows it
-            let refresh_token = match backend.create_refresh_token(
-                prepare_refresh_token(me.clone(), client_id, scope.clone())
-            ).await {
+            let refresh_token = match backend
+                .create_refresh_token(prepare_refresh_token(me.clone(), client_id, scope.clone()))
+                .await
+            {
                 Ok(token) => token,
                 Err(err) => {
                     tracing::error!("Error creating refresh token: {}", err);
@@ -626,24 +727,28 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
                 scope: Some(scope),
                 expires_in: Some(ACCESS_TOKEN_VALIDITY),
                 refresh_token: Some(refresh_token),
-                state: None
-            }.into_response()
-        },
+                state: None,
+            }
+            .into_response()
+        }
         GrantRequest::RefreshToken {
             refresh_token,
             client_id,
-            scope
+            scope,
         } => {
             let data = match backend.get_refresh_token(&me, &refresh_token).await {
                 Ok(Some(token)) => token,
-                Ok(None) => return Error {
-                    kind: ErrorKind::InvalidGrant,
-                    msg: Some("This refresh token is not valid.".to_string()),
-                    error_uri: None
-                }.into_response(),
+                Ok(None) => {
+                    return Error {
+                        kind: ErrorKind::InvalidGrant,
+                        msg: Some("This refresh token is not valid.".to_string()),
+                        error_uri: None,
+                    }
+                    .into_response()
+                }
                 Err(err) => {
                     tracing::error!("Error retrieving refresh token: {}", err);
-                    return StatusCode::INTERNAL_SERVER_ERROR.into_response()
+                    return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                 }
             };
 
@@ -651,17 +756,22 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
                 return Error {
                     kind: ErrorKind::InvalidGrant,
                     msg: Some("This refresh token is not yours.".to_string()),
-                    error_uri: None
-                }.into_response();
+                    error_uri: None,
+                }
+                .into_response();
             }
 
             let scope = if let Some(scope) = scope {
                 if !data.scope.has_all(scope.as_ref()) {
                     return Error {
                         kind: ErrorKind::InvalidScope,
-                        msg: Some("You can't request additional scopes through the refresh token grant.".to_string()),
-                        error_uri: None
-                    }.into_response();
+                        msg: Some(
+                            "You can't request additional scopes through the refresh token grant."
+                                .to_string(),
+                        ),
+                        error_uri: None,
+                    }
+                    .into_response();
                 }
 
                 scope
@@ -670,27 +780,27 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
                 data.scope
             };
 
-
             let profile = if scope.has(&Scope::Profile) {
-                match get_profile(
-                    db,
-                    data.me.as_str(),
-                    scope.has(&Scope::Email)
-                ).await {
+                match get_profile(db, data.me.as_str(), scope.has(&Scope::Email)).await {
                     Ok(profile) => profile,
                     Err(err) => {
                         tracing::error!("Error retrieving profile from database: {}", err);
 
-                        return StatusCode::INTERNAL_SERVER_ERROR.into_response()
+                        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                     }
                 }
             } else {
                 None
             };
 
-            let access_token = match backend.create_token(
-                prepare_access_token(data.me.clone(), client_id.clone(), scope.clone())
-            ).await {
+            let access_token = match backend
+                .create_token(prepare_access_token(
+                    data.me.clone(),
+                    client_id.clone(),
+                    scope.clone(),
+                ))
+                .await
+            {
                 Ok(token) => token,
                 Err(err) => {
                     tracing::error!("Error creating access token: {}", err);
@@ -699,9 +809,14 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
             };
 
             let old_refresh_token = refresh_token;
-            let refresh_token = match backend.create_refresh_token(
-                prepare_refresh_token(data.me.clone(), client_id, scope.clone())
-            ).await {
+            let refresh_token = match backend
+                .create_refresh_token(prepare_refresh_token(
+                    data.me.clone(),
+                    client_id,
+                    scope.clone(),
+                ))
+                .await
+            {
                 Ok(token) => token,
                 Err(err) => {
                     tracing::error!("Error creating refresh token: {}", err);
@@ -721,8 +836,9 @@ async fn token_endpoint_post<A: AuthBackend, D: Storage + 'static>(
                 scope: Some(scope),
                 expires_in: Some(ACCESS_TOKEN_VALIDITY),
                 refresh_token: Some(refresh_token),
-                state: None
-            }.into_response()
+                state: None,
+            }
+            .into_response()
         }
     }
 }
@@ -740,26 +856,39 @@ async fn introspection_endpoint_post<A: AuthBackend>(
 
     // Check authentication first
     match backend.get_token(&me, auth_token.token()).await {
-        Ok(Some(token)) => if !token.scope.has(&Scope::custom(KITTYBOX_TOKEN_STATUS)) {
-            return (StatusCode::UNAUTHORIZED, Json(json!({
-                "error": kittybox_indieauth::ResourceErrorKind::InsufficientScope
-            }))).into_response();
-        },
-        Ok(None) => return (StatusCode::UNAUTHORIZED, Json(json!({
-            "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
-        }))).into_response(),
+        Ok(Some(token)) => {
+            if !token.scope.has(&Scope::custom(KITTYBOX_TOKEN_STATUS)) {
+                return (
+                    StatusCode::UNAUTHORIZED,
+                    Json(json!({
+                        "error": kittybox_indieauth::ResourceErrorKind::InsufficientScope
+                    })),
+                )
+                    .into_response();
+            }
+        }
+        Ok(None) => {
+            return (
+                StatusCode::UNAUTHORIZED,
+                Json(json!({
+                    "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
+                })),
+            )
+                .into_response()
+        }
         Err(err) => {
             tracing::error!("Error retrieving token data for introspection: {}", err);
-            return StatusCode::INTERNAL_SERVER_ERROR.into_response()
+            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
         }
     }
-    let response: TokenIntrospectionResponse = match backend.get_token(&me, &token_request.token).await {
-        Ok(maybe_data) => maybe_data.into(),
-        Err(err) => {
-            tracing::error!("Error retrieving token data: {}", err);
-            return StatusCode::INTERNAL_SERVER_ERROR.into_response()
-        }
-    };
+    let response: TokenIntrospectionResponse =
+        match backend.get_token(&me, &token_request.token).await {
+            Ok(maybe_data) => maybe_data.into(),
+            Err(err) => {
+                tracing::error!("Error retrieving token data: {}", err);
+                return StatusCode::INTERNAL_SERVER_ERROR.into_response();
+            }
+        };
 
     response.into_response()
 }
@@ -787,7 +916,7 @@ async fn revocation_endpoint_post<A: AuthBackend>(
 async fn get_profile<D: Storage + 'static>(
     db: D,
     url: &str,
-    email: bool
+    email: bool,
 ) -> crate::database::Result<Option<Profile>> {
     fn get_first(v: serde_json::Value) -> Option<String> {
         match v {
@@ -796,10 +925,10 @@ async fn get_profile<D: Storage + 'static>(
                 match a.pop() {
                     Some(serde_json::Value::String(s)) => Some(s),
                     Some(serde_json::Value::Object(mut o)) => o.remove("value").and_then(get_first),
-                    _ => None
+                    _ => None,
                 }
-            },
-            _ => None
+            }
+            _ => None,
         }
     }
 
@@ -807,15 +936,26 @@ async fn get_profile<D: Storage + 'static>(
         // Ruthlessly manually destructure the MF2 document to save memory
         let mut properties = match mf2.as_object_mut().unwrap().remove("properties") {
             Some(serde_json::Value::Object(props)) => props,
-            _ => unreachable!()
+            _ => unreachable!(),
         };
         drop(mf2);
         let name = properties.remove("name").and_then(get_first);
-        let url = properties.remove("uid").and_then(get_first).and_then(|u| u.parse().ok());
-        let photo = properties.remove("photo").and_then(get_first).and_then(|u| u.parse().ok());
+        let url = properties
+            .remove("uid")
+            .and_then(get_first)
+            .and_then(|u| u.parse().ok());
+        let photo = properties
+            .remove("photo")
+            .and_then(get_first)
+            .and_then(|u| u.parse().ok());
         let email = properties.remove("name").and_then(get_first);
 
-        Profile { name, url, photo, email }
+        Profile {
+            name,
+            url,
+            photo,
+            email,
+        }
     }))
 }
 
@@ -823,7 +963,7 @@ async fn userinfo_endpoint_get<A: AuthBackend, D: Storage + 'static>(
     Host(host): Host,
     TypedHeader(Authorization(auth_token)): TypedHeader<Authorization<Bearer>>,
     State(backend): State<A>,
-    State(db): State<D>
+    State(db): State<D>,
 ) -> Response {
     use serde_json::json;
 
@@ -832,14 +972,22 @@ async fn userinfo_endpoint_get<A: AuthBackend, D: Storage + 'static>(
     match backend.get_token(&me, auth_token.token()).await {
         Ok(Some(token)) => {
             if token.expired() {
-                return (StatusCode::UNAUTHORIZED, Json(json!({
-                    "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
-                }))).into_response();
+                return (
+                    StatusCode::UNAUTHORIZED,
+                    Json(json!({
+                        "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
+                    })),
+                )
+                    .into_response();
             }
             if !token.scope.has(&Scope::Profile) {
-                return (StatusCode::UNAUTHORIZED, Json(json!({
-                    "error": kittybox_indieauth::ResourceErrorKind::InsufficientScope
-                }))).into_response();
+                return (
+                    StatusCode::UNAUTHORIZED,
+                    Json(json!({
+                        "error": kittybox_indieauth::ResourceErrorKind::InsufficientScope
+                    })),
+                )
+                    .into_response();
             }
 
             match get_profile(db, me.as_str(), token.scope.has(&Scope::Email)).await {
@@ -847,17 +995,19 @@ async fn userinfo_endpoint_get<A: AuthBackend, D: Storage + 'static>(
                 Ok(None) => Json(json!({
                     // We do this because ResourceErrorKind is IndieAuth errors only
                     "error": "invalid_request"
-                })).into_response(),
+                }))
+                .into_response(),
                 Err(err) => {
                     tracing::error!("Error retrieving profile from database: {}", err);
 
                     StatusCode::INTERNAL_SERVER_ERROR.into_response()
                 }
             }
-        },
+        }
         Ok(None) => Json(json!({
             "error": kittybox_indieauth::ResourceErrorKind::InvalidToken
-        })).into_response(),
+        }))
+        .into_response(),
         Err(err) => {
             tracing::error!("Error reading token: {}", err);
 
@@ -871,57 +1021,51 @@ where
     S: Storage + FromRef<St> + 'static,
     A: AuthBackend + FromRef<St>,
     reqwest_middleware::ClientWithMiddleware: FromRef<St>,
-    St: Clone + Send + Sync + 'static
+    St: Clone + Send + Sync + 'static,
 {
-    use axum::routing::{Router, get, post};
+    use axum::routing::{get, post, Router};
 
     Router::new()
         .nest(
             "/.kittybox/indieauth",
             Router::new()
-                .route("/metadata",
-                       get(metadata))
+                .route("/metadata", get(metadata))
                 .route(
                     "/auth",
                     get(authorization_endpoint_get::<A, S>)
-                        .post(authorization_endpoint_post::<A, S>))
-                .route(
-                    "/auth/confirm",
-                    post(authorization_endpoint_confirm::<A>))
-                .route(
-                    "/token",
-                    post(token_endpoint_post::<A, S>))
-                .route(
-                    "/token_status",
-                    post(introspection_endpoint_post::<A>))
-                .route(
-                    "/revoke_token",
-                    post(revocation_endpoint_post::<A>))
+                        .post(authorization_endpoint_post::<A, S>),
+                )
+                .route("/auth/confirm", post(authorization_endpoint_confirm::<A>))
+                .route("/token", post(token_endpoint_post::<A, S>))
+                .route("/token_status", post(introspection_endpoint_post::<A>))
+                .route("/revoke_token", post(revocation_endpoint_post::<A>))
+                .route("/userinfo", get(userinfo_endpoint_get::<A, S>))
                 .route(
-                    "/userinfo",
-                    get(userinfo_endpoint_get::<A, S>))
-
-                .route("/webauthn/pre_register",
-                       get(
-                           #[cfg(feature = "webauthn")] webauthn::webauthn_pre_register::<A, S>,
-                           #[cfg(not(feature = "webauthn"))] || std::future::ready(axum::http::StatusCode::NOT_FOUND)
-                       )
+                    "/webauthn/pre_register",
+                    get(
+                        #[cfg(feature = "webauthn")]
+                        webauthn::webauthn_pre_register::<A, S>,
+                        #[cfg(not(feature = "webauthn"))]
+                        || std::future::ready(axum::http::StatusCode::NOT_FOUND),
+                    ),
                 )
-                .layer(tower_http::cors::CorsLayer::new()
-                       .allow_methods([
-                           axum::http::Method::GET,
-                           axum::http::Method::POST
-                       ])
-                       .allow_origin(tower_http::cors::Any))
+                .layer(
+                    tower_http::cors::CorsLayer::new()
+                        .allow_methods([axum::http::Method::GET, axum::http::Method::POST])
+                        .allow_origin(tower_http::cors::Any),
+                ),
         )
         .route(
             "/.well-known/oauth-authorization-server",
-            get(|| std::future::ready(
-                (StatusCode::FOUND,
-                 [("Location",
-                   "/.kittybox/indieauth/metadata")]
-                ).into_response()
-            ))
+            get(|| {
+                std::future::ready(
+                    (
+                        StatusCode::FOUND,
+                        [("Location", "/.kittybox/indieauth/metadata")],
+                    )
+                        .into_response(),
+                )
+            }),
         )
 }
 
@@ -929,9 +1073,10 @@ where
 mod tests {
     #[test]
     fn test_deserialize_authorization_confirmation() {
-        use super::{Credential, AuthorizationConfirmation};
+        use super::{AuthorizationConfirmation, Credential};
 
-        let confirmation = serde_json::from_str::<AuthorizationConfirmation>(r#"{
+        let confirmation = serde_json::from_str::<AuthorizationConfirmation>(
+            r#"{
             "request":{
                 "response_type": "code",
                 "client_id": "https://quill.p3k.io/",
@@ -942,12 +1087,14 @@ mod tests {
                 "scope": "create+media"
             },
             "authorization_method": "swordfish"
-        }"#).unwrap();
+        }"#,
+        )
+        .unwrap();
 
         match confirmation.authorization_method {
             Credential::Password(password) => assert_eq!(password.as_str(), "swordfish"),
             #[allow(unreachable_patterns)]
-            other => panic!("Incorrect credential: {:?}", other)
+            other => panic!("Incorrect credential: {:?}", other),
         }
         assert_eq!(confirmation.request.state.as_ref(), "10101010");
     }
diff --git a/src/indieauth/webauthn.rs b/src/indieauth/webauthn.rs
index 0757e72..80d210c 100644
--- a/src/indieauth/webauthn.rs
+++ b/src/indieauth/webauthn.rs
@@ -1,10 +1,17 @@
 use axum::{
     extract::Json,
+    http::StatusCode,
     response::{IntoResponse, Response},
-    http::StatusCode, Extension
+    Extension,
+};
+use axum_extra::extract::{
+    cookie::{Cookie, CookieJar},
+    Host,
+};
+use axum_extra::{
+    headers::{authorization::Bearer, Authorization},
+    TypedHeader,
 };
-use axum_extra::extract::{Host, cookie::{CookieJar, Cookie}};
-use axum_extra::{TypedHeader, headers::{authorization::Bearer, Authorization}};
 
 use super::backend::AuthBackend;
 use crate::database::Storage;
@@ -12,40 +19,33 @@ 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()
-        }
-    }
+    ($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
+    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()
-                }
+            Some(mut hcard) => match hcard["properties"]["uid"][0].take() {
+                serde_json::Value::String(name) => name,
+                _ => String::default(),
             },
-            None => String::default()
+            None => String::default(),
         },
-        Err(err) => bail!("Error retrieving h-card: {}", err)
+        Err(err) => bail!("Error retrieving h-card: {}", err),
     };
 
-    let webauthn = webauthn::WebauthnBuilder::new(
-        &host,
-        &uid_url
-    )
+    let webauthn = webauthn::WebauthnBuilder::new(&host, &uid_url)
         .unwrap()
         .rp_name("Kittybox")
         .build()
@@ -58,10 +58,10 @@ pub async fn webauthn_pre_register<A: AuthBackend, D: Storage + 'static>(
         webauthn::prelude::Uuid::nil(),
         &uid,
         &display_name,
-        Some(vec![])
+        Some(vec![]),
     ) {
         Ok((challenge, state)) => (challenge, state),
-        Err(err) => bail!("Error generating WebAuthn registration data: {}", err)
+        Err(err) => bail!("Error generating WebAuthn registration data: {}", err),
     };
 
     match auth.persist_registration_challenge(&uid_url, state).await {
@@ -69,11 +69,12 @@ pub async fn webauthn_pre_register<A: AuthBackend, D: Storage + 'static>(
             cookies.add(
                 Cookie::build((CHALLENGE_ID_COOKIE, challenge_id))
                     .secure(true)
-                    .finish()
+                    .finish(),
             ),
-            Json(challenge)
-        ).into_response(),
-        Err(err) => bail!("Failed to persist WebAuthn challenge: {}", err)
+            Json(challenge),
+        )
+            .into_response(),
+        Err(err) => bail!("Failed to persist WebAuthn challenge: {}", err),
     }
 }
 
@@ -82,39 +83,36 @@ pub async fn webauthn_register<A: AuthBackend>(
     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>
+    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)
+        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::UNAUTHORIZED.into_response();
         }
     }
 
-    return StatusCode::OK.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
+    challenge_id: &str,
 ) -> std::io::Result<bool> {
     let host = website.host_str().unwrap();
 
-    let webauthn = webauthn::WebauthnBuilder::new(
-        host,
-        website
-    )
+    let webauthn = webauthn::WebauthnBuilder::new(host, website)
         .unwrap()
         .rp_name("Kittybox")
         .build()
@@ -122,12 +120,14 @@ pub(crate) async fn verify<A: AuthBackend>(
 
     match webauthn.finish_passkey_authentication(
         &credential,
-        &auth.retrieve_authentication_challenge(&website, challenge_id).await?
+        &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();