use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use chrono::{Duration, Utc};
use std::sync::Arc;
use uuid::Uuid;
use webauthn_rs::prelude::{CredentialID, PasskeyRegistration};
use crate::errors::AppError;
use crate::repositories::{WebAuthnChallenge, WebAuthnCredential, WebAuthnRepository};
use super::{RegistrationOptionsResponse, VerifyRegistrationRequest, WebAuthnService};
impl WebAuthnService {
pub async fn start_registration(
&self,
user_id: Uuid,
user_email: Option<&str>,
user_name: Option<&str>,
existing_credentials: &[WebAuthnCredential],
repo: &Arc<dyn WebAuthnRepository>,
) -> Result<RegistrationOptionsResponse, AppError> {
let webauthn = self.get_webauthn().await?;
let attachment = self.authenticator_attachment()?;
let policy = self.user_verification_policy();
let exclude_credentials: Vec<CredentialID> = existing_credentials
.iter()
.filter_map(|c| {
URL_SAFE_NO_PAD
.decode(&c.credential_id)
.ok()
.map(CredentialID::from)
})
.collect();
let display_name = user_name.unwrap_or(user_email.unwrap_or("User"));
let user_id_string = user_id.to_string();
let user_name_for_webauthn = user_email.unwrap_or(&user_id_string);
let (mut ccr, reg_state) = webauthn
.start_passkey_registration(
Uuid::from_bytes(*user_id.as_bytes()),
user_name_for_webauthn,
display_name,
Some(exclude_credentials),
)
.map_err(|e| {
AppError::Internal(anyhow::anyhow!(
"WebAuthn registration start failed: {:?}",
e
))
})?;
self.apply_registration_options(&mut ccr, attachment, policy)?;
let state_json = self.serialize_registration_state(®_state, attachment, policy)?;
let challenge_id = Uuid::new_v4();
let challenge = WebAuthnChallenge {
challenge_id,
user_id: Some(user_id),
state: state_json,
challenge_type: "register".to_string(),
created_at: Utc::now(),
expires_at: Utc::now() + Duration::seconds(self.config.challenge_ttl_seconds as i64),
};
repo.store_challenge(challenge).await?;
Ok(RegistrationOptionsResponse {
challenge_id,
options: ccr,
})
}
pub async fn finish_registration(
&self,
request: VerifyRegistrationRequest,
repo: &Arc<dyn WebAuthnRepository>,
) -> Result<WebAuthnCredential, AppError> {
let webauthn = self.get_webauthn().await?;
let challenge = repo
.consume_challenge(request.challenge_id)
.await?
.ok_or_else(|| AppError::Validation("Challenge expired or not found".into()))?;
if challenge.challenge_type != "register" {
return Err(AppError::Validation("Invalid challenge type".into()));
}
let user_id = challenge
.user_id
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("Missing user_id in challenge")))?;
let reg_state: PasskeyRegistration =
serde_json::from_str(&challenge.state).map_err(|e| AppError::Internal(e.into()))?;
let passkey = webauthn
.finish_passkey_registration(&request.credential, ®_state)
.map_err(|e| {
AppError::Validation(format!("Registration verification failed: {:?}", e))
})?;
let cred_id = URL_SAFE_NO_PAD.encode(passkey.cred_id());
let passkey_json =
serde_json::to_string(&passkey).map_err(|e| AppError::Internal(e.into()))?;
let mut credential = WebAuthnCredential::new(
user_id,
cred_id,
passkey_json,
0, true, );
credential.label = request.label;
let stored = repo.create_credential(credential).await?;
Ok(stored)
}
pub async fn start_registration_for_signup(
&self,
ephemeral_user_id: Uuid,
repo: &Arc<dyn WebAuthnRepository>,
) -> Result<RegistrationOptionsResponse, AppError> {
let webauthn = self.get_webauthn().await?;
let attachment = self.authenticator_attachment()?;
let policy = self.user_verification_policy();
let user_id_string = ephemeral_user_id.to_string();
let (mut ccr, reg_state) = webauthn
.start_passkey_registration(
Uuid::from_bytes(*ephemeral_user_id.as_bytes()),
&user_id_string,
"User",
None, )
.map_err(|e| {
AppError::Internal(anyhow::anyhow!(
"WebAuthn signup registration start failed: {:?}",
e
))
})?;
self.apply_registration_options(&mut ccr, attachment, policy)?;
let state_json = self.serialize_registration_state(®_state, attachment, policy)?;
let challenge_id = Uuid::new_v4();
let challenge = WebAuthnChallenge {
challenge_id,
user_id: None, state: state_json,
challenge_type: "register_new".to_string(),
created_at: Utc::now(),
expires_at: Utc::now() + Duration::seconds(self.config.challenge_ttl_seconds as i64),
};
repo.store_challenge(challenge).await?;
Ok(RegistrationOptionsResponse {
challenge_id,
options: ccr,
})
}
pub async fn finish_registration_for_signup(
&self,
request: VerifyRegistrationRequest,
repo: &Arc<dyn WebAuthnRepository>,
) -> Result<WebAuthnCredential, AppError> {
let webauthn = self.get_webauthn().await?;
let challenge = repo
.consume_challenge(request.challenge_id)
.await?
.ok_or_else(|| AppError::Validation("Challenge expired or not found".into()))?;
if challenge.challenge_type != "register_new" {
return Err(AppError::Validation("Invalid challenge type".into()));
}
let reg_state: PasskeyRegistration =
serde_json::from_str(&challenge.state).map_err(|e| AppError::Internal(e.into()))?;
let passkey = webauthn
.finish_passkey_registration(&request.credential, ®_state)
.map_err(|e| {
AppError::Validation(format!("Registration verification failed: {:?}", e))
})?;
let cred_id = URL_SAFE_NO_PAD.encode(passkey.cred_id());
let passkey_json =
serde_json::to_string(&passkey).map_err(|e| AppError::Internal(e.into()))?;
let mut credential = WebAuthnCredential::new(
Uuid::nil(), cred_id,
passkey_json,
0,
true,
);
credential.label = request.label;
Ok(credential)
}
}