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