1use chrono::Utc;
2use http::HeaderMap;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::{env, sync::LazyLock};
6
7use crate::audit::{AuthMethod, AuthMethodDetails, LoginContext};
8use crate::passkey::{
9 AuthenticationOptions, AuthenticatorResponse, CredentialId, CredentialSearchField,
10 PasskeyCredential, PasskeyError, PasskeyStore, RegisterCredential, RegistrationOptions,
11 commit_registration, finish_authentication, prepare_registration_storage, start_authentication,
12 start_registration, validate_registration_challenge, verify_session_then_finish_registration,
13};
14use crate::session::{User as SessionUser, UserId, new_session_header};
15use crate::userdb::{User, UserStore};
16
17use super::errors::CoordinationError;
18use super::login_history::{record_login_failure, record_login_success};
19use super::user::gen_new_user_id;
20
21static PASSKEY_USER_ACCOUNT_FIELD: LazyLock<String> =
23 LazyLock::new(|| env::var("PASSKEY_USER_ACCOUNT_FIELD").unwrap_or_else(|_| "name".to_string()));
24
25static PASSKEY_USER_LABEL_FIELD: LazyLock<String> = LazyLock::new(|| {
27 env::var("PASSKEY_USER_LABEL_FIELD").unwrap_or_else(|_| "display_name".to_string())
28});
29
30fn get_passkey_field_mappings() -> (String, String) {
32 (
33 PASSKEY_USER_ACCOUNT_FIELD.clone(),
34 PASSKEY_USER_LABEL_FIELD.clone(),
35 )
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "snake_case")]
45pub enum RegistrationMode {
46 AddToUser,
52
53 CreateUser,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct RegistrationStartRequest {
68 pub username: String,
70 pub displayname: String,
72 pub mode: RegistrationMode,
74}
75
76#[tracing::instrument(skip(auth_user), fields(user_id = auth_user.as_ref().map(|u| u.id.as_str()), username = %body.username, display_name = %body.displayname, mode = ?body.mode))]
81pub async fn handle_start_registration_core(
82 auth_user: Option<&SessionUser>,
83 body: RegistrationStartRequest,
84) -> Result<RegistrationOptions, CoordinationError> {
85 tracing::info!("Starting passkey registration flow");
86 match body.mode {
87 RegistrationMode::AddToUser => {
88 let auth_user = match auth_user {
89 Some(user) => user,
90 None => return Err(CoordinationError::Unauthorized.log()),
91 };
92
93 let result =
94 start_registration(Some(auth_user.clone()), body.username, body.displayname)
95 .await?;
96 Ok(result)
97 }
98 RegistrationMode::CreateUser => {
99 match auth_user {
100 Some(_) => return Err(CoordinationError::UnexpectedlyAuthorized.log()),
101 None => {
102 tracing::trace!("handle_start_registration_core: Create User");
103 }
104 };
105
106 let result = start_registration(None, body.username, body.displayname).await?;
107 Ok(result)
108 }
109 }
110}
111
112#[tracing::instrument(skip(auth_user, reg_data), fields(user_id = auth_user.as_ref().map(|u| u.id.as_str())))]
118pub async fn handle_finish_registration_core(
119 auth_user: Option<&SessionUser>,
120 reg_data: RegisterCredential,
121) -> Result<(HeaderMap, String), CoordinationError> {
122 tracing::info!("Finishing passkey registration flow");
123 match auth_user {
124 Some(session_user) => {
125 tracing::debug!("handle_finish_registration_core: User: {:#?}", session_user);
126
127 let message =
129 verify_session_then_finish_registration(session_user.clone(), reg_data).await?;
130
131 Ok((HeaderMap::new(), message))
132 }
133 None => {
134 let result = create_user_then_finish_registration(reg_data).await;
135
136 match result {
137 Ok((message, stored_user_id)) => {
138 let user_id = UserId::new(stored_user_id).map_err(|e| {
140 CoordinationError::Validation(format!("Invalid user ID: {e}"))
141 })?;
142 let headers = new_session_header(user_id).await?;
143
144 Ok((headers, message))
145 }
146 Err(err) => Err(err),
147 }
148 }
149 }
150}
151
152async fn create_user_then_finish_registration(
153 reg_data: RegisterCredential,
154) -> Result<(String, String), CoordinationError> {
155 let validated_data = validate_registration_challenge(®_data).await?;
163 let user_handle = validated_data.user_handle.clone();
164
165 let (account, label) = get_account_and_label_from_passkey(®_data).await;
167
168 let new_user = User {
169 id: gen_new_user_id().await?,
170 account,
171 label,
172 is_admin: *crate::config::O2P_DEMO_MODE,
173 sequence_number: None,
174 created_at: Utc::now(),
175 updated_at: Utc::now(),
176 };
177
178 let stored_user = UserStore::upsert_user(new_user).await?;
179
180 let user_id = UserId::new(stored_user.id.clone())
182 .map_err(|e| CoordinationError::Validation(format!("Invalid user ID: {e}")))?;
183 let credential = prepare_registration_storage(user_id, validated_data).await?;
184
185 let message = commit_registration(credential, &user_handle).await?;
187
188 Ok((message, stored_user.id))
189}
190
191async fn get_account_and_label_from_passkey(reg_data: &RegisterCredential) -> (String, String) {
192 let (name, display_name) = reg_data.get_registration_user_fields().await;
194
195 let (account_field, label_field) = get_passkey_field_mappings();
197
198 let account = match account_field.as_str() {
200 "name" => name.clone(),
201 "display_name" => display_name.clone(),
202 _ => name.clone(), };
204
205 let label = match label_field.as_str() {
206 "name" => name.clone(),
207 "display_name" => display_name.clone(),
208 _ => display_name.clone(), };
210 (account, label)
211}
212
213#[tracing::instrument(skip(body), fields(username))]
218pub async fn handle_start_authentication_core(
219 body: &Value,
220) -> Result<AuthenticationOptions, CoordinationError> {
221 tracing::info!("Starting passkey authentication flow");
222 let username = if body.is_object() {
224 body.get("username")
225 .and_then(|v| v.as_str())
226 .map(String::from)
227 } else if body.is_string() {
228 Some(body.as_str().unwrap().to_string()) } else {
230 None
231 };
232
233 if let Some(ref username) = username {
235 tracing::Span::current().record("username", username);
236 }
237
238 Ok(start_authentication(username).await?)
240}
241
242#[derive(Debug, Serialize)]
245pub struct AuthenticationResponse {
246 pub name: String,
248 pub user_handle: String,
250 pub credential_ids: Vec<String>,
252}
253
254#[tracing::instrument(skip(auth_response, headers), fields(user_id))]
264pub async fn handle_finish_authentication_core(
265 auth_response: AuthenticatorResponse,
266 headers: Option<&HeaderMap>,
267) -> Result<(AuthenticationResponse, HeaderMap), CoordinationError> {
268 tracing::info!("Finishing passkey authentication flow");
269 tracing::debug!("Auth response: {:#?}", auth_response);
270
271 let login_context = headers.map(LoginContext::from_headers).unwrap_or_default();
273
274 let credential_id_str = auth_response.credential_id().to_string();
276
277 let auth_result = match finish_authentication(auth_response).await {
279 Ok(result) => result,
280 Err(e) => {
281 record_auth_failure(login_context, credential_id_str, &e).await;
282 return Err(e.into());
283 }
284 };
285 let uid = auth_result.user_id;
286 let name = auth_result.user_name;
287 let user_handle = auth_result.user_handle;
288 let aaguid = auth_result.aaguid;
289
290 async fn record_auth_failure(
292 login_context: LoginContext,
293 credential_id_str: String,
294 error: &PasskeyError,
295 ) {
296 tracing::warn!(
297 credential_id = %credential_id_str,
298 error = %error,
299 "Passkey authentication failed"
300 );
301
302 let user_id = async {
304 let cred_id = CredentialId::new(credential_id_str.clone()).ok()?;
305 let cred = PasskeyStore::get_credential(cred_id).await.ok()??;
306 UserId::new(cred.user_id).ok()
307 }
308 .await;
309
310 let _ = record_login_failure(
311 user_id,
312 AuthMethod::Passkey,
313 login_context,
314 Some(credential_id_str),
315 error.to_string(),
316 )
317 .await;
318 }
319
320 tracing::Span::current().record("user_id", &uid);
322 tracing::info!(user_id = %uid, user_name = %name, "Passkey authentication successful");
323 tracing::debug!("User ID: {:#?}", uid);
324
325 let user_id = UserId::new(uid.clone())
327 .map_err(|e| CoordinationError::Validation(format!("Invalid user ID: {e}")))?;
328 let response_headers = new_session_header(user_id.clone()).await?;
329
330 let _ = record_login_success(
332 user_id.clone(),
333 AuthMethod::Passkey,
334 login_context,
335 AuthMethodDetails {
336 credential_id: Some(credential_id_str),
337 aaguid: Some(aaguid),
338 ..Default::default()
339 },
340 )
341 .await;
342
343 let credentials = list_credentials_core(user_id).await?;
345 let credential_ids = credentials
346 .iter()
347 .map(|c| c.credential_id.clone())
348 .collect();
349
350 Ok((
351 AuthenticationResponse {
352 name,
353 user_handle,
354 credential_ids,
355 },
356 response_headers,
357 ))
358}
359
360#[tracing::instrument(fields(user_id))]
365pub async fn list_credentials_core(
366 user_id: UserId,
367) -> Result<Vec<PasskeyCredential>, CoordinationError> {
368 tracing::debug!("Listing passkey credentials for user");
369 let credentials =
370 PasskeyStore::get_credentials_by(CredentialSearchField::UserId(user_id)).await?;
371 tracing::info!(
372 credential_count = credentials.len(),
373 "Retrieved passkey credentials"
374 );
375 Ok(credentials)
376}
377
378#[derive(Debug, Serialize)]
381pub struct DeleteCredentialResponse {
382 pub remaining_credential_ids: Vec<String>,
384 pub user_handle: String,
386}
387
388#[tracing::instrument(fields(user_id, credential_id))]
395pub async fn delete_passkey_credential_core(
396 user_id: UserId,
397 credential_id: CredentialId,
398) -> Result<DeleteCredentialResponse, CoordinationError> {
399 tracing::info!("Attempting to delete passkey credential");
400
401 let credential = PasskeyStore::get_credentials_by(CredentialSearchField::CredentialId(
402 credential_id.clone(),
403 ))
404 .await?
405 .into_iter()
406 .next()
407 .ok_or(
408 CoordinationError::ResourceNotFound {
409 resource_type: "Passkey".to_string(),
410 resource_id: credential_id.as_str().to_string(),
411 }
412 .log(),
413 )?;
414
415 if credential.user_id != user_id.as_str() {
417 return Err(CoordinationError::Unauthorized.log());
418 }
419
420 let user_handle = credential.user.user_handle.clone();
421
422 PasskeyStore::delete_credential_by(CredentialSearchField::CredentialId(credential_id)).await?;
424
425 let remaining = list_credentials_core(user_id).await?;
429 let remaining_credential_ids = remaining
430 .iter()
431 .filter(|c| c.user.user_handle == user_handle)
432 .map(|c| c.credential_id.clone())
433 .collect();
434
435 tracing::debug!("Successfully deleted credential");
436
437 Ok(DeleteCredentialResponse {
438 remaining_credential_ids,
439 user_handle,
440 })
441}
442
443#[tracing::instrument(skip(session_user), fields(user_id = session_user.as_ref().map(|u| u.id.as_str()), credential_id, name, display_name))]
457pub async fn update_passkey_credential_core(
458 credential_id: CredentialId,
459 name: &str,
460 display_name: &str,
461 session_user: Option<SessionUser>,
462) -> Result<serde_json::Value, CoordinationError> {
463 tracing::info!("Updating passkey credential details");
464 let user_id = match session_user {
466 Some(user) => user.id,
467 None => {
468 return Err(CoordinationError::Unauthorized.log());
469 }
470 };
471
472 let credential = match PasskeyStore::get_credential(credential_id.clone()).await? {
474 Some(cred) => cred,
475 None => {
476 return Err(CoordinationError::ResourceNotFound {
477 resource_type: "Passkey".to_string(),
478 resource_id: credential_id.as_str().to_string(),
479 });
480 }
481 };
482
483 if credential.user_id != user_id {
485 return Err(CoordinationError::Unauthorized.log());
486 }
487
488 PasskeyStore::update_credential(credential_id.clone(), name, display_name).await?;
490
491 let updated_credential = match PasskeyStore::get_credential(credential_id.clone()).await? {
493 Some(cred) => cred,
494 None => {
495 return Err(CoordinationError::ResourceNotFound {
496 resource_type: "Passkey".to_string(),
497 resource_id: credential_id.as_str().to_string(),
498 });
499 }
500 };
501
502 tracing::debug!("Successfully updated credential");
503
504 Ok(serde_json::json!({
506 "credentialId": credential_id.as_str(),
507 "name": updated_credential.user.name,
508 "displayName": updated_credential.user.display_name,
509 "userHandle": updated_credential.user.user_handle,
510 }))
511}
512
513#[cfg(test)]
514mod tests;