use super::{
AuthenticationResult, Passkey, PasskeyAuthentication, PasskeyError, PasskeyRelyingParty,
PublicKeyCredential, RequestChallengeResponse,
};
impl PasskeyRelyingParty {
pub fn start_authentication(
&self,
allow_credentials: &[Passkey],
) -> Result<(RequestChallengeResponse, PasskeyAuthentication), PasskeyError> {
self.webauthn
.start_passkey_authentication(allow_credentials)
.map_err(PasskeyError::Ceremony)
}
pub fn finish_authentication(
&self,
credential: &PublicKeyCredential,
state: &PasskeyAuthentication,
) -> Result<AuthenticationResult, PasskeyError> {
self.webauthn
.finish_passkey_authentication(credential, state)
.map_err(PasskeyError::Ceremony)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum PasskeyUpdate<'a> {
Updated(&'a Passkey),
Unchanged(&'a Passkey),
UnknownCredential,
}
pub fn apply_authentication_result<'a>(
passkeys: &'a mut [Passkey],
result: &AuthenticationResult,
) -> PasskeyUpdate<'a> {
let Some(passkey) = passkeys
.iter_mut()
.find(|p| p.cred_id() == result.cred_id())
else {
return PasskeyUpdate::UnknownCredential;
};
match passkey.update_credential(result) {
Some(true) => PasskeyUpdate::Updated(passkey),
_ => PasskeyUpdate::Unchanged(passkey),
}
}
#[cfg(test)]
mod tests {
use super::{apply_authentication_result, PasskeyUpdate};
use crate::passkey::{Passkey, PasskeyError, PasskeyRelyingParty, Url, Uuid};
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
use webauthn_authenticator_rs::WebauthnAuthenticator;
const RP_ID: &str = "example.com";
const ORIGIN: &str = "https://example.com";
fn rp() -> PasskeyRelyingParty {
PasskeyRelyingParty::new(RP_ID, Url::parse(ORIGIN).unwrap())
.expect("valid relying-party config")
}
fn register_with(
rp: &PasskeyRelyingParty,
authenticator: &mut WebauthnAuthenticator<SoftPasskey>,
) -> Passkey {
let (ccr, state) = rp
.start_registration(Uuid::new_v4(), "alice@example.com", "Alice", &[])
.expect("start_registration");
let credential = authenticator
.do_registration(Url::parse(ORIGIN).unwrap(), ccr)
.expect("software authenticator registration");
rp.finish_registration(&credential, &state)
.expect("finish_registration")
}
#[test]
fn start_authentication_challenge_has_expected_shape() {
let rp = rp();
let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true));
let passkey = register_with(&rp, &mut authenticator);
let (rcr, _state) = rp
.start_authentication(std::slice::from_ref(&passkey))
.unwrap();
let json = serde_json::to_value(&rcr).unwrap();
let pk = &json["publicKey"];
assert_eq!(pk["rpId"], "example.com");
assert!(!pk["challenge"].as_str().unwrap().is_empty());
assert_eq!(pk["userVerification"], "required");
let want_id = serde_json::to_value(passkey.cred_id()).unwrap();
let allowed = pk["allowCredentials"]
.as_array()
.expect("allowCredentials present");
assert!(
allowed.iter().any(|c| c["id"] == want_id),
"allowCredentials {allowed:?} should list the stored cred_id {want_id}"
);
}
#[test]
fn authenticate_round_trip_identifies_the_credential() {
let rp = rp();
let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true));
let passkey = register_with(&rp, &mut authenticator);
let (rcr, state) = rp
.start_authentication(std::slice::from_ref(&passkey))
.unwrap();
let assertion = authenticator
.do_authentication(Url::parse(ORIGIN).unwrap(), rcr)
.expect("software authenticator authentication");
let result = rp.finish_authentication(&assertion, &state).unwrap();
assert_eq!(
result.cred_id().as_slice(),
passkey.cred_id().as_slice(),
"the result names the credential that answered"
);
}
#[test]
fn finish_rejects_an_assertion_bound_to_a_different_challenge() {
let rp = rp();
let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true));
let passkey = register_with(&rp, &mut authenticator);
let (rcr1, _state1) = rp
.start_authentication(std::slice::from_ref(&passkey))
.unwrap();
let assertion = authenticator
.do_authentication(Url::parse(ORIGIN).unwrap(), rcr1)
.unwrap();
let (_rcr2, state2) = rp
.start_authentication(std::slice::from_ref(&passkey))
.unwrap();
let err = rp.finish_authentication(&assertion, &state2).unwrap_err();
assert!(matches!(err, PasskeyError::Ceremony(_)), "got {err:?}");
}
#[test]
fn apply_result_updates_the_answering_passkey() {
let rp = rp();
let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true));
let passkey = register_with(&rp, &mut authenticator);
let (rcr, state) = rp
.start_authentication(std::slice::from_ref(&passkey))
.unwrap();
let assertion = authenticator
.do_authentication(Url::parse(ORIGIN).unwrap(), rcr)
.unwrap();
let result = rp.finish_authentication(&assertion, &state).unwrap();
let mut stored = vec![passkey.clone()];
match apply_authentication_result(&mut stored, &result) {
PasskeyUpdate::Updated(p) | PasskeyUpdate::Unchanged(p) => {
assert_eq!(p.cred_id().as_slice(), passkey.cred_id().as_slice());
}
PasskeyUpdate::UnknownCredential => {
panic!("the answering passkey is in the stored set")
}
}
}
#[test]
fn apply_result_flags_an_unknown_credential() {
let rp = rp();
let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true));
let passkey = register_with(&rp, &mut authenticator);
let (rcr, state) = rp
.start_authentication(std::slice::from_ref(&passkey))
.unwrap();
let assertion = authenticator
.do_authentication(Url::parse(ORIGIN).unwrap(), rcr)
.unwrap();
let result = rp.finish_authentication(&assertion, &state).unwrap();
let mut none: Vec<Passkey> = Vec::new();
assert!(matches!(
apply_authentication_result(&mut none, &result),
PasskeyUpdate::UnknownCredential
));
}
}