mod authenticate;
mod register;
pub use authenticate::{apply_authentication_result, PasskeyUpdate};
pub use register::{passkey_from_credential, passkey_to_credential};
use std::time::Duration;
use webauthn_rs::prelude::{Webauthn, WebauthnBuilder, WebauthnError};
pub use webauthn_rs::prelude::{
AuthenticationResult, CreationChallengeResponse, CredentialID, Passkey, PasskeyAuthentication,
PasskeyRegistration, PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse,
Url, Uuid,
};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PasskeyError {
#[error("invalid passkey relying-party configuration: {0}")]
Config(#[source] WebauthnError),
#[error("passkey ceremony failed: {0}")]
Ceremony(#[source] WebauthnError),
#[error("serialising passkey credential: {0}")]
Serialize(#[source] serde_json::Error),
#[error("deserialising passkey credential: {0}")]
Deserialize(#[source] serde_json::Error),
#[error("stored credential is not a passkey binding (found {found:?})")]
WrongBinding {
found: cheers_core::DeviceBinding,
},
}
pub struct PasskeyRelyingParty {
webauthn: Webauthn,
rp_id: String,
rp_origin: Url,
rp_name: Option<String>,
extra_origins: Vec<Url>,
allow_subdomains: bool,
}
impl PasskeyRelyingParty {
pub fn new(rp_id: impl Into<String>, rp_origin: Url) -> Result<Self, PasskeyError> {
Self::builder(rp_id, rp_origin).build()
}
pub fn builder(rp_id: impl Into<String>, rp_origin: Url) -> PasskeyRelyingPartyBuilder {
PasskeyRelyingPartyBuilder {
rp_id: rp_id.into(),
rp_origin,
rp_name: None,
extra_origins: Vec::new(),
allow_subdomains: false,
timeout: None,
}
}
pub fn rp_id(&self) -> &str {
&self.rp_id
}
pub fn rp_origin(&self) -> &Url {
&self.rp_origin
}
pub fn rp_name(&self) -> Option<&str> {
self.rp_name.as_deref()
}
pub fn allowed_origins(&self) -> &[Url] {
self.webauthn.get_allowed_origins()
}
pub fn webauthn(&self) -> &Webauthn {
&self.webauthn
}
}
impl std::fmt::Debug for PasskeyRelyingParty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PasskeyRelyingParty")
.field("rp_id", &self.rp_id)
.field("rp_origin", &self.rp_origin.as_str())
.field("rp_name", &self.rp_name)
.field("allow_subdomains", &self.allow_subdomains)
.field("extra_origins", &self.extra_origins.len())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct PasskeyRelyingPartyBuilder {
rp_id: String,
rp_origin: Url,
rp_name: Option<String>,
extra_origins: Vec<Url>,
allow_subdomains: bool,
timeout: Option<Duration>,
}
impl PasskeyRelyingPartyBuilder {
pub fn rp_name(mut self, rp_name: impl Into<String>) -> Self {
self.rp_name = Some(rp_name.into());
self
}
pub fn allow_subdomains(mut self, allow: bool) -> Self {
self.allow_subdomains = allow;
self
}
pub fn append_allowed_origin(mut self, origin: Url) -> Self {
self.extra_origins.push(origin);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn build(self) -> Result<PasskeyRelyingParty, PasskeyError> {
let mut builder = WebauthnBuilder::new(&self.rp_id, &self.rp_origin)
.map_err(PasskeyError::Config)?
.allow_subdomains(self.allow_subdomains);
if let Some(rp_name) = self.rp_name.as_deref() {
builder = builder.rp_name(rp_name);
}
for origin in &self.extra_origins {
builder = builder.append_allowed_origin(origin);
}
if let Some(timeout) = self.timeout {
builder = builder.timeout(timeout);
}
let webauthn = builder.build().map_err(PasskeyError::Config)?;
Ok(PasskeyRelyingParty {
webauthn,
rp_id: self.rp_id,
rp_origin: self.rp_origin,
rp_name: self.rp_name,
extra_origins: self.extra_origins,
allow_subdomains: self.allow_subdomains,
})
}
}