use super::PasskeyHandlerOutcome;
use super::PasskeyHandlerResult;
use super::challenge_not_found;
use super::generation_origin;
use super::passkey_authentication_failure;
use super::passkey_not_found;
use super::response_code;
use super::response_message;
use crate::helpers::SessionIssueError;
use crate::helpers::issue_user_session_record;
use crate::passkey::PasskeyConfig;
use crate::passkey::types::SessionResponse;
use crate::passkey::types::VerifyAuthenticationRequest;
use crate::passkey::webauthn::StoredAuthenticationState;
use crate::passkey::webauthn::StoredRegistrationState;
use crate::passkey::webauthn::authentication_options_json;
use crate::passkey::webauthn::build_verification_core;
use crate::passkey::webauthn::build_webauthn;
use crate::passkey::webauthn::challenge_cookie_name;
use crate::passkey::webauthn::create_challenge_cookie;
use crate::passkey::webauthn::credential_id_from_authentication;
use crate::passkey::webauthn::decode_challenge_cookie;
use crate::passkey::webauthn::decode_credential_id;
use crate::passkey::webauthn::finish_core_authentication;
use crate::passkey::webauthn::get_cookie_value;
use crate::passkey::webauthn::parse_transports_csv;
use crate::passkey::webauthn::snapshot_passkey;
use alibi_core::AuthContext;
use alibi_core::AuthError;
use alibi_core::AuthPasskey;
use alibi_core::AuthResult;
use alibi_core::AuthSession;
use alibi_core::CreateVerification;
use alibi_core::entity::AuthUser;
use alibi_core::types::UpdatePasskeyAuthentication;
use base64::Engine;
use chrono::Duration;
use chrono::Utc;
use serde_json::Value;
use serde_json::json;
use uuid::Uuid;
use webauthn_rs::prelude::DiscoverableKey;
use webauthn_rs::prelude::PublicKeyCredential;
use webauthn_rs_core::error::WebauthnError;
use webauthn_rs_core::proto::UserVerificationPolicy;
pub(in crate::passkey) async fn generate_authenticate_options_core<U: AuthUser>(
maybe_user: Option<&U>,
extensions: Option<Value>,
config: &PasskeyConfig,
ctx: &AuthContext<impl alibi_core::AuthSchema>,
) -> AuthResult<(Value, String)> {
let core = build_verification_core(config, &ctx.config, &generation_origin(config, ctx))?;
let stored_passkeys = if let Some(user) = maybe_user {
ctx.database.list_passkeys_by_user(&user.id()).await?
} else {
Vec::new()
};
let allow_credentials_json = stored_passkeys
.iter()
.map(|passkey| {
let mut descriptor = json!({
"id": passkey.credential_id(),
"type": "public-key",
});
if let Some(transports) = passkey.transports().map(parse_transports_csv)
&& let Some(object) = descriptor.as_object_mut()
{
_ = object.insert("transports".to_owned(), json!(transports));
}
descriptor
})
.collect::<Vec<_>>();
let builder = core
.new_challenge_authenticate_builder(Vec::new(), Some(UserVerificationPolicy::Preferred))
.map_err(|error| {
AuthError::internal(format!("Failed to generate authenticate options: {error}"))
})?
.allow_backup_eligible_upgrade(true);
let (options, state) = core
.generate_challenge_authenticate(builder)
.map_err(|error| {
AuthError::internal(format!("Failed to generate authenticate options: {error}"))
})?;
let state = StoredAuthenticationState::CoreRaw {
challenge: base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode(options.public_key.challenge.as_ref()),
state,
};
let token = Uuid::new_v4().to_string();
let expires_at = Utc::now() + Duration::seconds(config.challenge_ttl_secs);
_ = ctx
.verifications()
.create(CreateVerification {
identifier: token.clone(),
value: serde_json::to_string(&state)?,
expires_at,
})
.await?;
let cookie = create_challenge_cookie(&ctx.config, config.challenge_ttl_secs, &token, config)?;
let mut response = authentication_options_json(options)?;
if let Some(extensions) = extensions {
if !extensions.is_object() {
return Err(AuthError::bad_request(
"Passkey extensions must be an object",
));
}
if let Some(object) = response.as_object_mut() {
_ = object.insert("extensions".into(), extensions);
}
}
if let Some(object) = response.as_object_mut() {
if allow_credentials_json.is_empty() {
_ = object.remove("allowCredentials");
} else {
_ = object.insert(
"allowCredentials".to_owned(),
Value::Array(allow_credentials_json),
);
}
}
Ok((response, cookie))
}
#[expect(
clippy::too_many_lines,
reason = "Keep credential verification, counter updates, and session callbacks in protocol order"
)]
pub(in crate::passkey) async fn verify_authentication_core<S: alibi_core::AuthSchema>(
body: &VerifyAuthenticationRequest,
req: &alibi_core::AuthRequest,
config: &PasskeyConfig,
ip_address: Option<String>,
user_agent: Option<String>,
ctx: &AuthContext<S>,
) -> PasskeyHandlerResult<(Value, String)> {
let Some(origin) =
crate::passkey::webauthn::ceremony_origin(config, req, body.response.as_ref())
else {
return response_message(400, "origin missing");
};
let Some(cookie_value) = get_cookie_value(req, &challenge_cookie_name(&ctx.config, config))
else {
return challenge_not_found();
};
let Ok(token) = decode_challenge_cookie(&ctx.config, &cookie_value) else {
return challenge_not_found();
};
let Some(verification) = ctx.verifications().consume(&token).await? else {
return challenge_not_found();
};
let stored_state: StoredAuthenticationState = match serde_json::from_str(verification.value()?)
{
Ok(state) => state,
Err(_)
if serde_json::from_str::<StoredRegistrationState>(verification.value()?).is_ok() =>
{
return challenge_not_found();
}
Err(_) => return passkey_authentication_failure(),
};
let Some(response) = body.response.as_ref() else {
return passkey_authentication_failure();
};
let authentication: PublicKeyCredential =
match serde_json::from_value(response.to_json_value()?) {
Ok(authentication) => authentication,
Err(_) => return passkey_authentication_failure(),
};
let credential_id = credential_id_from_authentication(&authentication);
let Some(passkey) = ctx
.database
.get_passkey_by_credential_id(&credential_id)
.await?
else {
return passkey_not_found();
};
let Ok(mut stored) =
serde_json::from_str::<crate::passkey::raw_none::StoredCredential>(passkey.credential())
else {
return passkey_authentication_failure();
};
let Ok(counter) = u32::try_from(passkey.counter()) else {
return passkey_authentication_failure();
};
if matches!(
&stored_state,
StoredAuthenticationState::Core { .. } | StoredAuthenticationState::CoreRaw { .. }
) {
let Ok(public_key) = base64::engine::general_purpose::STANDARD.decode(passkey.public_key())
else {
return passkey_authentication_failure();
};
match &mut stored {
crate::passkey::raw_none::StoredCredential::Raw(raw) => {
raw.replace_public_key(public_key);
}
crate::passkey::raw_none::StoredCredential::Core(saved) => {
let Ok((key, _)) = crate::passkey::raw_none::decode_first(&public_key) else {
return passkey_authentication_failure();
};
let Ok(key) = crate::passkey::source::crypto::COSEKey::try_from(&key) else {
return passkey_authentication_failure();
};
let mut current = saved.cred.clone();
current.cred = key;
current.cred_id = decode_credential_id(passkey.credential_id())?;
saved.cred = current;
}
}
}
let authentication_result = match (&mut stored, stored_state) {
(
crate::passkey::raw_none::StoredCredential::Raw(raw),
StoredAuthenticationState::CoreRaw { challenge, .. },
) => crate::passkey::raw_none::authenticate_raw(
raw,
&authentication,
response,
&challenge,
&crate::passkey::webauthn::resolve_rp_id(config, &ctx.config)?,
&origin,
counter,
),
(crate::passkey::raw_none::StoredCredential::Raw(_), _) => {
return passkey_authentication_failure();
}
(crate::passkey::raw_none::StoredCredential::Core(stored_passkey), state) => {
let Ok(webauthn) = build_webauthn(config, &ctx.config, &origin) else {
return passkey_authentication_failure();
};
let result = match state {
StoredAuthenticationState::Core { state }
| StoredAuthenticationState::CoreRaw { state, .. } => {
let mut current = stored_passkey.cred.clone();
current.counter = counter;
stored_passkey.cred = current;
let Ok(core) = build_verification_core(config, &ctx.config, &origin) else {
return passkey_authentication_failure();
};
finish_core_authentication(
&core,
&authentication,
&state,
stored_passkey,
counter,
&origin,
)
}
StoredAuthenticationState::Passkey { state } => {
webauthn.finish_passkey_authentication(&authentication, &state)
}
StoredAuthenticationState::Discoverable { state } => {
let Ok(legacy) = stored_passkey.to_registry() else {
return passkey_authentication_failure();
};
let discoverable_key = DiscoverableKey::from(legacy);
webauthn.finish_discoverable_authentication(
&authentication,
state,
&[discoverable_key],
)
}
};
result.map(crate::passkey::authentication::AuthenticationResult::Core)
}
};
let authentication_result = match authentication_result {
Ok(result) => result,
Err(WebauthnError::AuthenticationFailure) => {
return response_code(401, "AUTHENTICATION_FAILED", "Authentication failed");
}
Err(_) => return passkey_authentication_failure(),
};
let verified_owner = passkey.user_id().into_owned();
let mut public_backed_up = passkey.backed_up();
let mut public_device_type = passkey.device_type().to_owned();
if let Some(callback) = &config.authentication.after_verification {
let context = crate::passkey::PasskeyAuthenticationContext {
request: req,
auth_config: &ctx.config,
extensions: &ctx.extensions,
};
let verified = crate::passkey::VerifiedPasskeyAuthentication {
result: authentication_result.clone(),
origin: origin.clone(),
rp_id: crate::passkey::webauthn::resolve_rp_id(config, &ctx.config)?,
};
match callback
.after_verification(&context, &verified, response)
.await
{
Ok(()) => {}
Err(error) if crate::passkey::registration::is_application_error(&error) => {
return Err(error);
}
Err(_) => return passkey_authentication_failure(),
}
match ctx.database.get_passkey_by_id(passkey.id().as_ref()).await {
Ok(Some(current)) => {
public_backed_up = current.backed_up();
current.device_type().clone_into(&mut public_device_type);
}
Ok(None) => {}
Err(_) => return passkey_authentication_failure(),
}
}
let snapshot = match (&mut stored, &authentication_result) {
(
crate::passkey::raw_none::StoredCredential::Core(passkey),
crate::passkey::authentication::AuthenticationResult::Core(result),
) => {
if passkey.update_credential(result).is_none() {
return passkey_authentication_failure();
}
snapshot_passkey(passkey)
}
(
crate::passkey::raw_none::StoredCredential::Raw(raw),
crate::passkey::authentication::AuthenticationResult::Raw(result),
) => {
raw.apply_authentication(result);
raw.snapshot()
}
_ => return passkey_authentication_failure(),
};
let Ok(snapshot) = snapshot else {
return passkey_authentication_failure();
};
match ctx
.database
.update_passkey_authentication(
passkey.id().as_ref(),
UpdatePasskeyAuthentication {
credential: snapshot.serialized,
counter: snapshot.counter,
backed_up: public_backed_up,
device_type: public_device_type,
},
)
.await
{
Ok(_) => {}
Err(_) => return passkey_authentication_failure(),
}
let Some(user) = ctx.database.get_user_by_id_record(&verified_owner).await? else {
return response_message(500, "User not found");
};
let session = match issue_user_session_record(ctx, &user.id(), ip_address, user_agent)
.await
.map_err(SessionIssueError::into_auth_error)
{
Ok(issued) => issued.session,
Err(error) => return Err(error),
};
let Some(user) = ctx.database.get_user_by_id_record(&verified_owner).await? else {
return response_message(500, "User not found");
};
crate::helpers::record_completed_session_user_view::<S>(
&user,
&session,
ctx.trusted_user_view(&user),
);
Ok(PasskeyHandlerOutcome::Success((
serde_json::to_value(SessionResponse {
session: ctx.session_view(&session),
user: ctx.trusted_user_view(&user),
})?,
session.token().to_owned(),
)))
}