Something went wrong. Try again.
Our Personal Data Server from scratch!
Something went wrong. Try again.
8.0 kB · 272 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273use axum::{Json, extract::State};use serde::{Deserialize, Serialize};use tracing::{error, info, warn};use tranquil_db_traits::WebauthnChallengeType;use tranquil_pds::api::EmptyResponse;use tranquil_pds::api::error::{ApiError, DbResultExt};use tranquil_pds::auth::{Active, Auth, require_legacy_session_mfa, require_reauth_window};use tranquil_pds::state::AppState;use webauthn_rs::prelude::*;
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]pub struct StartRegistrationInput { pub friendly_name: Option<String>,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct StartRegistrationOutput { pub options: serde_json::Value,}
pub async fn start_passkey_registration( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<StartRegistrationInput>,) -> Result<Json<StartRegistrationOutput>, ApiError> { let webauthn = &state.webauthn_config;
let handle = state .repos .user .get_handle_by_did(&auth.did) .await .log_db_err("fetching user")? .ok_or(ApiError::AccountNotFound)?;
let existing_passkeys = state .repos .user .get_passkeys_for_user(&auth.did) .await .log_db_err("fetching existing passkeys")?;
let exclude_credentials: Vec<CredentialID> = existing_passkeys .iter() .map(|p| CredentialID::from(p.credential_id.clone())) .collect();
let display_name = input.friendly_name.as_deref().unwrap_or(&handle);
let (ccr, reg_state) = webauthn .start_registration(&auth.did, &handle, display_name, exclude_credentials) .map_err(|e| { error!("Failed to start passkey registration: {}", e); ApiError::InternalError(Some("Failed to start registration".into())) })?;
let state_json = serde_json::to_string(®_state).map_err(|e| { error!("Failed to serialize registration state: {:?}", e); ApiError::InternalError(None) })?;
state .repos .user .save_webauthn_challenge(&auth.did, WebauthnChallengeType::Registration, &state_json) .await .log_db_err("saving registration state")?;
let options = serde_json::to_value(&ccr).unwrap_or(serde_json::json!({}));
info!(did = %auth.did, "Passkey registration started");
Ok(Json(StartRegistrationOutput { options }))}
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]pub struct FinishRegistrationInput { pub credential: serde_json::Value, pub friendly_name: Option<String>,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct FinishRegistrationOutput { pub id: String, pub credential_id: String,}
pub async fn finish_passkey_registration( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<FinishRegistrationInput>,) -> Result<Json<FinishRegistrationOutput>, ApiError> { let webauthn = &state.webauthn_config;
let reg_state_json = state .repos .user .load_webauthn_challenge(&auth.did, WebauthnChallengeType::Registration) .await .log_db_err("loading registration state")? .ok_or(ApiError::NoRegistrationInProgress)?;
let reg_state: SecurityKeyRegistration = serde_json::from_str(®_state_json).map_err(|e| { error!("Failed to deserialize registration state: {:?}", e); ApiError::InternalError(None) })?;
let credential: RegisterPublicKeyCredential = serde_json::from_value(input.credential) .map_err(|e| { warn!("Failed to parse credential: {:?}", e); ApiError::InvalidCredential })?;
let passkey = webauthn .finish_registration(&credential, ®_state) .map_err(|e| { warn!("Failed to finish passkey registration: {}", e); ApiError::RegistrationFailed })?;
let public_key = serde_json::to_vec(&passkey).map_err(|e| { error!("Failed to serialize passkey: {:?}", e); ApiError::InternalError(None) })?;
let passkey_id = state .repos .user .save_passkey( &auth.did, passkey.cred_id(), &public_key, input.friendly_name.as_deref(), ) .await .log_db_err("saving passkey")?;
if let Err(e) = state .repos .user .delete_webauthn_challenge(&auth.did, WebauthnChallengeType::Registration) .await { warn!("Failed to delete registration state: {:?}", e); }
let credential_id_base64 = base64::Engine::encode( &base64::engine::general_purpose::URL_SAFE_NO_PAD, passkey.cred_id(), );
info!(did = %auth.did, passkey_id = %passkey_id, "Passkey registered");
Ok(Json(FinishRegistrationOutput { id: passkey_id.to_string(), credential_id: credential_id_base64, }))}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct PasskeyInfo { pub id: String, pub credential_id: String, pub friendly_name: Option<String>, pub created_at: String, pub last_used: Option<String>,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct ListPasskeysOutput { pub passkeys: Vec<PasskeyInfo>,}
pub async fn list_passkeys( State(state): State<AppState>, auth: Auth<Active>,) -> Result<Json<ListPasskeysOutput>, ApiError> { let passkeys = state .repos .user .get_passkeys_for_user(&auth.did) .await .log_db_err("fetching passkeys")?;
let passkey_infos: Vec<PasskeyInfo> = passkeys .into_iter() .map(|pk| PasskeyInfo { id: pk.id.to_string(), credential_id: pk.credential_id_base64(), friendly_name: pk.friendly_name, created_at: pk.created_at.to_rfc3339(), last_used: pk.last_used.map(|dt| dt.to_rfc3339()), }) .collect();
Ok(Json(ListPasskeysOutput { passkeys: passkey_infos, }))}
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]pub struct DeletePasskeyInput { pub id: String,}
pub async fn delete_passkey( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<DeletePasskeyInput>,) -> Result<Json<EmptyResponse>, ApiError> { let session_mfa = require_legacy_session_mfa(&state, &auth).await?;
let reauth_mfa = require_reauth_window(&state, &auth).await?;
let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
match state.repos.user.delete_passkey(id, reauth_mfa.did()).await { Ok(true) => { info!(did = %session_mfa.did(), passkey_id = %id, "Passkey deleted"); Ok(Json(EmptyResponse {})) } Ok(false) => Err(ApiError::PasskeyNotFound), Err(e) => { error!("DB error deleting passkey: {:?}", e); Err(ApiError::InternalError(None)) } }}
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]pub struct UpdatePasskeyInput { pub id: String, pub friendly_name: String,}
pub async fn update_passkey( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<UpdatePasskeyInput>,) -> Result<Json<EmptyResponse>, ApiError> { let id: uuid::Uuid = input.id.parse().map_err(|_| ApiError::InvalidId)?;
match state .repos .user .update_passkey_name(id, &auth.did, &input.friendly_name) .await { Ok(true) => { info!(did = %auth.did, passkey_id = %id, "Passkey renamed"); Ok(Json(EmptyResponse {})) } Ok(false) => Err(ApiError::PasskeyNotFound), Err(e) => { error!("DB error updating passkey: {:?}", e); Err(ApiError::InternalError(None)) } }}
pub async fn has_passkeys_for_user(state: &AppState, did: &tranquil_pds::types::Did) -> bool { state.repos.user.has_passkeys(did).await.unwrap_or(false)}