this repo has no description
at main 9.4 kB view raw
1use crate::api::EmptyResponse; 2use crate::api::error::ApiError; 3use crate::auth::BearerAuth; 4use crate::auth::webauthn::{ 5 self, WebAuthnConfig, delete_passkey as db_delete_passkey, delete_registration_state, 6 get_passkeys_for_user, load_registration_state, save_passkey, save_registration_state, 7 update_passkey_name as db_update_passkey_name, 8}; 9use crate::state::AppState; 10use axum::{ 11 Json, 12 extract::State, 13 response::{IntoResponse, Response}, 14}; 15use serde::{Deserialize, Serialize}; 16use tracing::{error, info, warn}; 17use webauthn_rs::prelude::*; 18 19fn get_webauthn() -> Result<WebAuthnConfig, ApiError> { 20 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 21 WebAuthnConfig::new(&hostname).map_err(|e| { 22 error!("Failed to create WebAuthn config: {}", e); 23 ApiError::InternalError(Some("WebAuthn configuration failed".into())) 24 }) 25} 26 27#[derive(Deserialize)] 28#[serde(rename_all = "camelCase")] 29pub struct StartRegistrationInput { 30 pub friendly_name: Option<String>, 31} 32 33#[derive(Serialize)] 34#[serde(rename_all = "camelCase")] 35pub struct StartRegistrationResponse { 36 pub options: serde_json::Value, 37} 38 39pub async fn start_passkey_registration( 40 State(state): State<AppState>, 41 auth: BearerAuth, 42 Json(input): Json<StartRegistrationInput>, 43) -> Response { 44 let webauthn = match get_webauthn() { 45 Ok(w) => w, 46 Err(e) => return e.into_response(), 47 }; 48 49 let user = sqlx::query!("SELECT handle FROM users WHERE did = $1", &*auth.0.did) 50 .fetch_optional(&state.db) 51 .await; 52 53 let handle = match user { 54 Ok(Some(row)) => row.handle, 55 Ok(None) => { 56 return ApiError::AccountNotFound.into_response(); 57 } 58 Err(e) => { 59 error!("DB error fetching user: {:?}", e); 60 return ApiError::InternalError(None).into_response(); 61 } 62 }; 63 64 let existing_passkeys = match get_passkeys_for_user(&state.db, &auth.0.did).await { 65 Ok(passkeys) => passkeys, 66 Err(e) => { 67 error!("DB error fetching existing passkeys: {:?}", e); 68 return ApiError::InternalError(None).into_response(); 69 } 70 }; 71 72 let exclude_credentials: Vec<CredentialID> = existing_passkeys 73 .iter() 74 .map(|p| CredentialID::from(p.credential_id.clone())) 75 .collect(); 76 77 let display_name = input.friendly_name.as_deref().unwrap_or(&handle); 78 79 let (ccr, reg_state) = match webauthn.start_registration( 80 &auth.0.did, 81 &handle, 82 display_name, 83 exclude_credentials, 84 ) { 85 Ok(result) => result, 86 Err(e) => { 87 error!("Failed to start passkey registration: {}", e); 88 return ApiError::InternalError(Some("Failed to start registration".into())) 89 .into_response(); 90 } 91 }; 92 93 if let Err(e) = save_registration_state(&state.db, &auth.0.did, &reg_state).await { 94 error!("Failed to save registration state: {:?}", e); 95 return ApiError::InternalError(None).into_response(); 96 } 97 98 let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({})); 99 100 info!(did = %auth.0.did, "Passkey registration started"); 101 102 Json(StartRegistrationResponse { options }).into_response() 103} 104 105#[derive(Deserialize)] 106#[serde(rename_all = "camelCase")] 107pub struct FinishRegistrationInput { 108 pub credential: serde_json::Value, 109 pub friendly_name: Option<String>, 110} 111 112#[derive(Serialize)] 113#[serde(rename_all = "camelCase")] 114pub struct FinishRegistrationResponse { 115 pub id: String, 116 pub credential_id: String, 117} 118 119pub async fn finish_passkey_registration( 120 State(state): State<AppState>, 121 auth: BearerAuth, 122 Json(input): Json<FinishRegistrationInput>, 123) -> Response { 124 let webauthn = match get_webauthn() { 125 Ok(w) => w, 126 Err(e) => return e.into_response(), 127 }; 128 129 let reg_state = match load_registration_state(&state.db, &auth.0.did).await { 130 Ok(Some(state)) => state, 131 Ok(None) => { 132 return ApiError::NoRegistrationInProgress.into_response(); 133 } 134 Err(e) => { 135 error!("DB error loading registration state: {:?}", e); 136 return ApiError::InternalError(None).into_response(); 137 } 138 }; 139 140 let credential: RegisterPublicKeyCredential = match serde_json::from_value(input.credential) { 141 Ok(c) => c, 142 Err(e) => { 143 warn!("Failed to parse credential: {:?}", e); 144 return ApiError::InvalidCredential.into_response(); 145 } 146 }; 147 148 let passkey = match webauthn.finish_registration(&credential, &reg_state) { 149 Ok(pk) => pk, 150 Err(e) => { 151 warn!("Failed to finish passkey registration: {}", e); 152 return ApiError::RegistrationFailed.into_response(); 153 } 154 }; 155 156 let passkey_id = match save_passkey( 157 &state.db, 158 &auth.0.did, 159 &passkey, 160 input.friendly_name.as_deref(), 161 ) 162 .await 163 { 164 Ok(id) => id, 165 Err(e) => { 166 error!("Failed to save passkey: {:?}", e); 167 return ApiError::InternalError(None).into_response(); 168 } 169 }; 170 171 if let Err(e) = delete_registration_state(&state.db, &auth.0.did).await { 172 warn!("Failed to delete registration state: {:?}", e); 173 } 174 175 let credential_id_base64 = base64::Engine::encode( 176 &base64::engine::general_purpose::URL_SAFE_NO_PAD, 177 passkey.cred_id(), 178 ); 179 180 info!(did = %auth.0.did, passkey_id = %passkey_id, "Passkey registered"); 181 182 Json(FinishRegistrationResponse { 183 id: passkey_id.to_string(), 184 credential_id: credential_id_base64, 185 }) 186 .into_response() 187} 188 189#[derive(Serialize)] 190#[serde(rename_all = "camelCase")] 191pub struct PasskeyInfo { 192 pub id: String, 193 pub credential_id: String, 194 pub friendly_name: Option<String>, 195 pub created_at: String, 196 pub last_used: Option<String>, 197} 198 199#[derive(Serialize)] 200#[serde(rename_all = "camelCase")] 201pub struct ListPasskeysResponse { 202 pub passkeys: Vec<PasskeyInfo>, 203} 204 205pub async fn list_passkeys(State(state): State<AppState>, auth: BearerAuth) -> Response { 206 let passkeys = match get_passkeys_for_user(&state.db, &auth.0.did).await { 207 Ok(pks) => pks, 208 Err(e) => { 209 error!("DB error fetching passkeys: {:?}", e); 210 return ApiError::InternalError(None).into_response(); 211 } 212 }; 213 214 let passkey_infos: Vec<PasskeyInfo> = passkeys 215 .into_iter() 216 .map(|pk| PasskeyInfo { 217 id: pk.id.to_string(), 218 credential_id: pk.credential_id_base64(), 219 friendly_name: pk.friendly_name, 220 created_at: pk.created_at.to_rfc3339(), 221 last_used: pk.last_used.map(|dt| dt.to_rfc3339()), 222 }) 223 .collect(); 224 225 Json(ListPasskeysResponse { 226 passkeys: passkey_infos, 227 }) 228 .into_response() 229} 230 231#[derive(Deserialize)] 232#[serde(rename_all = "camelCase")] 233pub struct DeletePasskeyInput { 234 pub id: String, 235} 236 237pub async fn delete_passkey( 238 State(state): State<AppState>, 239 auth: BearerAuth, 240 Json(input): Json<DeletePasskeyInput>, 241) -> Response { 242 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &auth.0.did).await { 243 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &auth.0.did) 244 .await; 245 } 246 247 if crate::api::server::reauth::check_reauth_required(&state.db, &auth.0.did).await { 248 return crate::api::server::reauth::reauth_required_response(&state.db, &auth.0.did).await; 249 } 250 251 let id: uuid::Uuid = match input.id.parse() { 252 Ok(id) => id, 253 Err(_) => { 254 return ApiError::InvalidId.into_response(); 255 } 256 }; 257 258 match db_delete_passkey(&state.db, id, &auth.0.did).await { 259 Ok(true) => { 260 info!(did = %auth.0.did, passkey_id = %id, "Passkey deleted"); 261 EmptyResponse::ok().into_response() 262 } 263 Ok(false) => ApiError::PasskeyNotFound.into_response(), 264 Err(e) => { 265 error!("DB error deleting passkey: {:?}", e); 266 ApiError::InternalError(None).into_response() 267 } 268 } 269} 270 271#[derive(Deserialize)] 272#[serde(rename_all = "camelCase")] 273pub struct UpdatePasskeyInput { 274 pub id: String, 275 pub friendly_name: String, 276} 277 278pub async fn update_passkey( 279 State(state): State<AppState>, 280 auth: BearerAuth, 281 Json(input): Json<UpdatePasskeyInput>, 282) -> Response { 283 let id: uuid::Uuid = match input.id.parse() { 284 Ok(id) => id, 285 Err(_) => { 286 return ApiError::InvalidId.into_response(); 287 } 288 }; 289 290 match db_update_passkey_name(&state.db, id, &auth.0.did, &input.friendly_name).await { 291 Ok(true) => { 292 info!(did = %auth.0.did, passkey_id = %id, "Passkey renamed"); 293 EmptyResponse::ok().into_response() 294 } 295 Ok(false) => ApiError::PasskeyNotFound.into_response(), 296 Err(e) => { 297 error!("DB error updating passkey: {:?}", e); 298 ApiError::InternalError(None).into_response() 299 } 300 } 301} 302 303pub async fn has_passkeys_for_user(state: &AppState, did: &str) -> bool { 304 has_passkeys_for_user_db(&state.db, did).await 305} 306 307pub async fn has_passkeys_for_user_db(db: &sqlx::PgPool, did: &str) -> bool { 308 webauthn::has_passkeys(db, did).await.unwrap_or(false) 309}