use rand_core::CryptoRng;
use time::{Duration, OffsetDateTime};
use url::Url;
use webauthn_rs::prelude::{
AuthenticationResult, CreationChallengeResponse, DiscoverableAuthentication, DiscoverableKey,
Passkey, PasskeyAuthentication, PasskeyRegistration, PublicKeyCredential,
RegisterPublicKeyCredential, RequestChallengeResponse,
};
use webauthn_rs::{Webauthn, WebauthnBuilder};
use crate::Error;
use crate::config::Config;
use crate::ids::{CeremonyId, SessionId, UserId};
pub use webauthn_rs;
pub const CEREMONY_TTL: Duration = Duration::minutes(5);
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[must_use = "store the ceremony to finish it later"]
pub struct Ceremony<S> {
id: CeremonyId,
state: S,
session: Option<SessionId>,
user: Option<UserId>,
expires_at: OffsetDateTime,
}
impl<S> Ceremony<S> {
#[must_use]
pub fn id(&self) -> CeremonyId {
self.id
}
#[must_use]
pub fn expires_at(&self) -> OffsetDateTime {
self.expires_at
}
pub fn into_state(
self,
session: Option<SessionId>,
user: Option<UserId>,
now: OffsetDateTime,
) -> Result<S, Error> {
if self.expires_at <= now || self.session != session || self.user != user {
return Err(Error::InvalidRequest("unknown_ceremony"));
}
Ok(self.state)
}
}
#[derive(Debug)]
pub struct RelyingParty {
webauthn: Webauthn,
}
impl RelyingParty {
pub fn new(config: &Config) -> Result<Self, Error> {
Self::from_parts(config.rp_id(), config.rp_name(), config.origin())
}
pub fn from_parts(rp_id: &str, rp_name: &str, origin: &Url) -> Result<Self, Error> {
let webauthn = WebauthnBuilder::new(rp_id, origin)
.and_then(|builder| builder.rp_name(rp_name).build())
.map_err(|_| Error::Config("rp_id does not match the origin"))?;
Ok(Self { webauthn })
}
#[allow(clippy::too_many_arguments)]
pub fn start_registration<R: CryptoRng + ?Sized>(
&self,
user: UserId,
user_name: &str,
display_name: &str,
existing: &[Passkey],
session: SessionId,
now: OffsetDateTime,
rng: &mut R,
) -> Result<(CreationChallengeResponse, Ceremony<PasskeyRegistration>), Error> {
let exclude = existing.iter().map(|key| key.cred_id().clone()).collect();
let (challenge, state) = self
.webauthn
.start_passkey_registration(user.into_uuid(), user_name, display_name, Some(exclude))
.map_err(|_| Error::Crypto("webauthn registration start"))?;
Ok((
challenge,
ceremony(state, Some(session), Some(user), now, rng),
))
}
pub fn finish_registration(
&self,
credential: &RegisterPublicKeyCredential,
state: &PasskeyRegistration,
existing: &[Passkey],
) -> Result<Passkey, Error> {
let passkey = self
.webauthn
.finish_passkey_registration(credential, state)
.map_err(|_| Error::InvalidRequest("passkey_rejected"))?;
if existing
.iter()
.any(|key| key.cred_id() == passkey.cred_id())
{
return Err(Error::Conflict("passkey_exists"));
}
Ok(passkey)
}
pub fn start_authentication<R: CryptoRng + ?Sized>(
&self,
user: UserId,
passkeys: &[Passkey],
session: SessionId,
now: OffsetDateTime,
rng: &mut R,
) -> Result<(RequestChallengeResponse, Ceremony<PasskeyAuthentication>), Error> {
if passkeys.is_empty() {
return Err(Error::InvalidRequest("no_passkey"));
}
let (challenge, state) = self
.webauthn
.start_passkey_authentication(passkeys)
.map_err(|_| Error::Crypto("webauthn authentication start"))?;
Ok((
challenge,
ceremony(state, Some(session), Some(user), now, rng),
))
}
pub fn finish_authentication(
&self,
credential: &PublicKeyCredential,
state: &PasskeyAuthentication,
) -> Result<AuthenticationResult, Error> {
self.webauthn
.finish_passkey_authentication(credential, state)
.map_err(|_| Error::SignInFailed)
}
pub fn start_discoverable<R: CryptoRng + ?Sized>(
&self,
now: OffsetDateTime,
rng: &mut R,
) -> Result<
(
RequestChallengeResponse,
Ceremony<DiscoverableAuthentication>,
),
Error,
> {
let (challenge, state) = self
.webauthn
.start_discoverable_authentication()
.map_err(|_| Error::Crypto("webauthn discoverable start"))?;
Ok((challenge, ceremony(state, None, None, now, rng)))
}
pub fn identify_discoverable(&self, credential: &PublicKeyCredential) -> Result<UserId, Error> {
self.webauthn
.identify_discoverable_authentication(credential)
.map(|(user, _)| UserId::from_uuid(user))
.map_err(|_| Error::SignInFailed)
}
pub fn finish_discoverable(
&self,
credential: &PublicKeyCredential,
state: DiscoverableAuthentication,
user: UserId,
passkeys: &[Passkey],
) -> Result<AuthenticationResult, Error> {
if self.identify_discoverable(credential)? != user {
return Err(Error::SignInFailed);
}
let keys: Vec<DiscoverableKey> = passkeys.iter().map(DiscoverableKey::from).collect();
self.webauthn
.finish_discoverable_authentication(credential, state, &keys)
.map_err(|_| Error::SignInFailed)
}
}
#[must_use]
pub fn record_use(passkeys: &mut [Passkey], result: &AuthenticationResult) -> Option<usize> {
passkeys
.iter_mut()
.position(|passkey| passkey.update_credential(result).is_some())
}
fn ceremony<S, R: CryptoRng + ?Sized>(
state: S,
session: Option<SessionId>,
user: Option<UserId>,
now: OffsetDateTime,
rng: &mut R,
) -> Ceremony<S> {
Ceremony {
id: CeremonyId::random(rng),
state,
session,
user,
expires_at: now.saturating_add(CEREMONY_TTL),
}
}