use chrono::Utc;
use http::HeaderMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{env, sync::LazyLock};
use crate::audit::{AuthMethod, AuthMethodDetails, LoginContext};
use crate::passkey::{
AuthenticationOptions, AuthenticatorResponse, CredentialId, CredentialSearchField,
PasskeyCredential, PasskeyError, PasskeyStore, RegisterCredential, RegistrationOptions,
commit_registration, finish_authentication, prepare_registration_storage, start_authentication,
start_registration, validate_registration_challenge, verify_session_then_finish_registration,
};
use crate::session::{User as SessionUser, UserId, new_session_header};
use crate::userdb::{User, UserStore};
use super::errors::CoordinationError;
use super::login_history::{record_login_failure, record_login_success};
use super::user::gen_new_user_id;
static PASSKEY_USER_ACCOUNT_FIELD: LazyLock<String> =
LazyLock::new(|| env::var("PASSKEY_USER_ACCOUNT_FIELD").unwrap_or_else(|_| "name".to_string()));
static PASSKEY_USER_LABEL_FIELD: LazyLock<String> = LazyLock::new(|| {
env::var("PASSKEY_USER_LABEL_FIELD").unwrap_or_else(|_| "display_name".to_string())
});
fn get_passkey_field_mappings() -> (String, String) {
(
PASSKEY_USER_ACCOUNT_FIELD.clone(),
PASSKEY_USER_LABEL_FIELD.clone(),
)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RegistrationMode {
AddToUser,
CreateUser,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistrationStartRequest {
pub username: String,
pub displayname: String,
pub mode: RegistrationMode,
}
#[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))]
pub async fn handle_start_registration_core(
auth_user: Option<&SessionUser>,
body: RegistrationStartRequest,
) -> Result<RegistrationOptions, CoordinationError> {
tracing::info!("Starting passkey registration flow");
match body.mode {
RegistrationMode::AddToUser => {
let auth_user = match auth_user {
Some(user) => user,
None => return Err(CoordinationError::Unauthorized.log()),
};
let result =
start_registration(Some(auth_user.clone()), body.username, body.displayname)
.await?;
Ok(result)
}
RegistrationMode::CreateUser => {
match auth_user {
Some(_) => return Err(CoordinationError::UnexpectedlyAuthorized.log()),
None => {
tracing::trace!("handle_start_registration_core: Create User");
}
};
let result = start_registration(None, body.username, body.displayname).await?;
Ok(result)
}
}
}
#[tracing::instrument(skip(auth_user, reg_data), fields(user_id = auth_user.as_ref().map(|u| u.id.as_str())))]
pub async fn handle_finish_registration_core(
auth_user: Option<&SessionUser>,
reg_data: RegisterCredential,
) -> Result<(HeaderMap, String), CoordinationError> {
tracing::info!("Finishing passkey registration flow");
match auth_user {
Some(session_user) => {
tracing::debug!("handle_finish_registration_core: User: {:#?}", session_user);
let message =
verify_session_then_finish_registration(session_user.clone(), reg_data).await?;
Ok((HeaderMap::new(), message))
}
None => {
let result = create_user_then_finish_registration(reg_data).await;
match result {
Ok((message, stored_user_id)) => {
let user_id = UserId::new(stored_user_id).map_err(|e| {
CoordinationError::Validation(format!("Invalid user ID: {e}"))
})?;
let headers = new_session_header(user_id).await?;
Ok((headers, message))
}
Err(err) => Err(err),
}
}
}
}
async fn create_user_then_finish_registration(
reg_data: RegisterCredential,
) -> Result<(String, String), CoordinationError> {
let validated_data = validate_registration_challenge(®_data).await?;
let user_handle = validated_data.user_handle.clone();
let (account, label) = get_account_and_label_from_passkey(®_data).await;
let new_user = User {
id: gen_new_user_id().await?,
account,
label,
is_admin: *crate::config::O2P_DEMO_MODE,
sequence_number: None,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let stored_user = UserStore::upsert_user(new_user).await?;
let user_id = UserId::new(stored_user.id.clone())
.map_err(|e| CoordinationError::Validation(format!("Invalid user ID: {e}")))?;
let credential = prepare_registration_storage(user_id, validated_data).await?;
let message = commit_registration(credential, &user_handle).await?;
Ok((message, stored_user.id))
}
async fn get_account_and_label_from_passkey(reg_data: &RegisterCredential) -> (String, String) {
let (name, display_name) = reg_data.get_registration_user_fields().await;
let (account_field, label_field) = get_passkey_field_mappings();
let account = match account_field.as_str() {
"name" => name.clone(),
"display_name" => display_name.clone(),
_ => name.clone(), };
let label = match label_field.as_str() {
"name" => name.clone(),
"display_name" => display_name.clone(),
_ => display_name.clone(), };
(account, label)
}
#[tracing::instrument(skip(body), fields(username))]
pub async fn handle_start_authentication_core(
body: &Value,
) -> Result<AuthenticationOptions, CoordinationError> {
tracing::info!("Starting passkey authentication flow");
let username = if body.is_object() {
body.get("username")
.and_then(|v| v.as_str())
.map(String::from)
} else if body.is_string() {
Some(body.as_str().unwrap().to_string()) } else {
None
};
if let Some(ref username) = username {
tracing::Span::current().record("username", username);
}
Ok(start_authentication(username).await?)
}
#[derive(Debug, Serialize)]
pub struct AuthenticationResponse {
pub name: String,
pub user_handle: String,
pub credential_ids: Vec<String>,
}
#[tracing::instrument(skip(auth_response, headers), fields(user_id))]
pub async fn handle_finish_authentication_core(
auth_response: AuthenticatorResponse,
headers: Option<&HeaderMap>,
) -> Result<(AuthenticationResponse, HeaderMap), CoordinationError> {
tracing::info!("Finishing passkey authentication flow");
tracing::debug!("Auth response: {:#?}", auth_response);
let login_context = headers.map(LoginContext::from_headers).unwrap_or_default();
let credential_id_str = auth_response.credential_id().to_string();
let auth_result = match finish_authentication(auth_response).await {
Ok(result) => result,
Err(e) => {
record_auth_failure(login_context, credential_id_str, &e).await;
return Err(e.into());
}
};
let uid = auth_result.user_id;
let name = auth_result.user_name;
let user_handle = auth_result.user_handle;
let aaguid = auth_result.aaguid;
async fn record_auth_failure(
login_context: LoginContext,
credential_id_str: String,
error: &PasskeyError,
) {
tracing::warn!(
credential_id = %credential_id_str,
error = %error,
"Passkey authentication failed"
);
let user_id = async {
let cred_id = CredentialId::new(credential_id_str.clone()).ok()?;
let cred = PasskeyStore::get_credential(cred_id).await.ok()??;
UserId::new(cred.user_id).ok()
}
.await;
let _ = record_login_failure(
user_id,
AuthMethod::Passkey,
login_context,
Some(credential_id_str),
error.to_string(),
)
.await;
}
tracing::Span::current().record("user_id", &uid);
tracing::info!(user_id = %uid, user_name = %name, "Passkey authentication successful");
tracing::debug!("User ID: {:#?}", uid);
let user_id = UserId::new(uid.clone())
.map_err(|e| CoordinationError::Validation(format!("Invalid user ID: {e}")))?;
let response_headers = new_session_header(user_id.clone()).await?;
let _ = record_login_success(
user_id.clone(),
AuthMethod::Passkey,
login_context,
AuthMethodDetails {
credential_id: Some(credential_id_str),
aaguid: Some(aaguid),
..Default::default()
},
)
.await;
let credentials = list_credentials_core(user_id).await?;
let credential_ids = credentials
.iter()
.map(|c| c.credential_id.clone())
.collect();
Ok((
AuthenticationResponse {
name,
user_handle,
credential_ids,
},
response_headers,
))
}
#[tracing::instrument(fields(user_id))]
pub async fn list_credentials_core(
user_id: UserId,
) -> Result<Vec<PasskeyCredential>, CoordinationError> {
tracing::debug!("Listing passkey credentials for user");
let credentials =
PasskeyStore::get_credentials_by(CredentialSearchField::UserId(user_id)).await?;
tracing::info!(
credential_count = credentials.len(),
"Retrieved passkey credentials"
);
Ok(credentials)
}
#[derive(Debug, Serialize)]
pub struct DeleteCredentialResponse {
pub remaining_credential_ids: Vec<String>,
pub user_handle: String,
}
#[tracing::instrument(fields(user_id, credential_id))]
pub async fn delete_passkey_credential_core(
user_id: UserId,
credential_id: CredentialId,
) -> Result<DeleteCredentialResponse, CoordinationError> {
tracing::info!("Attempting to delete passkey credential");
let credential = PasskeyStore::get_credentials_by(CredentialSearchField::CredentialId(
credential_id.clone(),
))
.await?
.into_iter()
.next()
.ok_or(
CoordinationError::ResourceNotFound {
resource_type: "Passkey".to_string(),
resource_id: credential_id.as_str().to_string(),
}
.log(),
)?;
if credential.user_id != user_id.as_str() {
return Err(CoordinationError::Unauthorized.log());
}
let user_handle = credential.user.user_handle.clone();
PasskeyStore::delete_credential_by(CredentialSearchField::CredentialId(credential_id)).await?;
let remaining = list_credentials_core(user_id).await?;
let remaining_credential_ids = remaining
.iter()
.filter(|c| c.user.user_handle == user_handle)
.map(|c| c.credential_id.clone())
.collect();
tracing::debug!("Successfully deleted credential");
Ok(DeleteCredentialResponse {
remaining_credential_ids,
user_handle,
})
}
#[tracing::instrument(skip(session_user), fields(user_id = session_user.as_ref().map(|u| u.id.as_str()), credential_id, name, display_name))]
pub async fn update_passkey_credential_core(
credential_id: CredentialId,
name: &str,
display_name: &str,
session_user: Option<SessionUser>,
) -> Result<serde_json::Value, CoordinationError> {
tracing::info!("Updating passkey credential details");
let user_id = match session_user {
Some(user) => user.id,
None => {
return Err(CoordinationError::Unauthorized.log());
}
};
let credential = match PasskeyStore::get_credential(credential_id.clone()).await? {
Some(cred) => cred,
None => {
return Err(CoordinationError::ResourceNotFound {
resource_type: "Passkey".to_string(),
resource_id: credential_id.as_str().to_string(),
});
}
};
if credential.user_id != user_id {
return Err(CoordinationError::Unauthorized.log());
}
PasskeyStore::update_credential(credential_id.clone(), name, display_name).await?;
let updated_credential = match PasskeyStore::get_credential(credential_id.clone()).await? {
Some(cred) => cred,
None => {
return Err(CoordinationError::ResourceNotFound {
resource_type: "Passkey".to_string(),
resource_id: credential_id.as_str().to_string(),
});
}
};
tracing::debug!("Successfully updated credential");
Ok(serde_json::json!({
"credentialId": credential_id.as_str(),
"name": updated_credential.user.name,
"displayName": updated_credential.user.display_name,
"userHandle": updated_credential.user.user_handle,
}))
}
#[cfg(test)]
mod tests;