openauth-passkey 0.0.5

Server-side passkey plugin for OpenAuth.
Documentation
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::webauthn::{PasskeyWebAuthnBackend, RealPasskeyWebAuthnBackend};

/// Advanced passkey plugin settings.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PasskeyAdvancedOptions {
    pub webauthn_challenge_cookie: String,
}

impl Default for PasskeyAdvancedOptions {
    fn default() -> Self {
        Self {
            webauthn_challenge_cookie: "better-auth-passkey".to_owned(),
        }
    }
}

/// Passkey plugin settings.
#[derive(Clone)]
pub struct PasskeyOptions {
    pub rp_id: Option<String>,
    pub rp_name: Option<String>,
    pub origin: Vec<String>,
    pub passkey_table: String,
    pub authenticator_selection: AuthenticatorSelection,
    pub registration: PasskeyRegistrationOptions,
    pub authentication: PasskeyAuthenticationOptions,
    pub advanced: PasskeyAdvancedOptions,
    pub backend: Arc<dyn PasskeyWebAuthnBackend>,
}

impl Default for PasskeyOptions {
    fn default() -> Self {
        Self {
            rp_id: None,
            rp_name: None,
            origin: Vec::new(),
            passkey_table: "passkeys".to_owned(),
            authenticator_selection: AuthenticatorSelection::default(),
            registration: PasskeyRegistrationOptions::default(),
            authentication: PasskeyAuthenticationOptions::default(),
            advanced: PasskeyAdvancedOptions::default(),
            backend: Arc::new(RealPasskeyWebAuthnBackend),
        }
    }
}

impl PasskeyOptions {
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn rp_id(mut self, rp_id: impl Into<String>) -> Self {
        self.rp_id = Some(rp_id.into());
        self
    }

    #[must_use]
    pub fn rp_name(mut self, rp_name: impl Into<String>) -> Self {
        self.rp_name = Some(rp_name.into());
        self
    }

    #[must_use]
    pub fn origin(mut self, origin: impl Into<String>) -> Self {
        self.origin.push(origin.into());
        self
    }

    #[must_use]
    pub fn passkey_table(mut self, table: impl Into<String>) -> Self {
        self.passkey_table = table.into();
        self
    }

    #[must_use]
    pub fn authenticator_selection(mut self, selection: AuthenticatorSelection) -> Self {
        self.authenticator_selection = selection;
        self
    }

    #[must_use]
    pub fn registration(mut self, registration: PasskeyRegistrationOptions) -> Self {
        self.registration = registration;
        self
    }

    #[must_use]
    pub fn authentication(mut self, authentication: PasskeyAuthenticationOptions) -> Self {
        self.authentication = authentication;
        self
    }

    #[must_use]
    pub fn advanced(mut self, advanced: PasskeyAdvancedOptions) -> Self {
        self.advanced = advanced;
        self
    }

    #[must_use]
    pub fn backend(mut self, backend: Arc<dyn PasskeyWebAuthnBackend>) -> Self {
        self.backend = backend;
        self
    }
}

/// Browser authenticator attachment hint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthenticatorAttachment {
    Platform,
    CrossPlatform,
}

impl AuthenticatorAttachment {
    pub(crate) fn from_query(value: &str) -> Option<Self> {
        match value {
            "platform" => Some(Self::Platform),
            "cross-platform" => Some(Self::CrossPlatform),
            _ => None,
        }
    }

    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::Platform => "platform",
            Self::CrossPlatform => "cross-platform",
        }
    }
}

/// Resident key preference used in registration options.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResidentKeyRequirement {
    Discouraged,
    Preferred,
    Required,
}

impl ResidentKeyRequirement {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::Discouraged => "discouraged",
            Self::Preferred => "preferred",
            Self::Required => "required",
        }
    }
}

/// User verification preference used in WebAuthn options.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum UserVerificationRequirement {
    Discouraged,
    Preferred,
    Required,
}

impl UserVerificationRequirement {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::Discouraged => "discouraged",
            Self::Preferred => "preferred",
            Self::Required => "required",
        }
    }
}

/// Authenticator selection hints for generated registration options.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthenticatorSelection {
    pub resident_key: ResidentKeyRequirement,
    pub user_verification: UserVerificationRequirement,
    pub authenticator_attachment: Option<AuthenticatorAttachment>,
}

impl Default for AuthenticatorSelection {
    fn default() -> Self {
        Self {
            resident_key: ResidentKeyRequirement::Preferred,
            user_verification: UserVerificationRequirement::Preferred,
            authenticator_attachment: None,
        }
    }
}

impl AuthenticatorSelection {
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn resident_key(mut self, resident_key: ResidentKeyRequirement) -> Self {
        self.resident_key = resident_key;
        self
    }

    #[must_use]
    pub fn user_verification(mut self, user_verification: UserVerificationRequirement) -> Self {
        self.user_verification = user_verification;
        self
    }

    #[must_use]
    pub fn authenticator_attachment(mut self, attachment: AuthenticatorAttachment) -> Self {
        self.authenticator_attachment = Some(attachment);
        self
    }

    pub(crate) fn with_attachment_override(
        &self,
        attachment: Option<AuthenticatorAttachment>,
    ) -> Self {
        let mut selection = self.clone();
        if attachment.is_some() {
            selection.authenticator_attachment = attachment;
        }
        selection
    }

    pub fn to_json(&self) -> Value {
        let mut value = json!({
            "residentKey": self.resident_key.as_str(),
            "userVerification": self.user_verification.as_str(),
        });
        if let Some(attachment) = self.authenticator_attachment {
            value["authenticatorAttachment"] = json!(attachment.as_str());
        }
        value
    }
}

/// WebAuthn option customizations resolved for one registration request.
#[derive(Debug, Clone, PartialEq)]
pub struct RegistrationWebAuthnOptions {
    pub authenticator_selection: AuthenticatorSelection,
    pub extensions: Option<Value>,
}

impl RegistrationWebAuthnOptions {
    pub(crate) fn new(
        authenticator_selection: AuthenticatorSelection,
        extensions: Option<Value>,
    ) -> Self {
        Self {
            authenticator_selection,
            extensions,
        }
    }
}

/// User identity used for passkey registration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PasskeyRegistrationUser {
    pub id: String,
    pub name: String,
    pub display_name: Option<String>,
}

impl PasskeyRegistrationUser {
    pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            name: name.into(),
            display_name: None,
        }
    }

    #[must_use]
    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
        self.display_name = Some(display_name.into());
        self
    }
}

pub type ResolveRegistrationUser =
    Arc<dyn Fn(ResolveRegistrationUserInput) -> Option<PasskeyRegistrationUser> + Send + Sync>;

pub type AfterRegistrationVerification =
    Arc<dyn Fn(AfterRegistrationVerificationInput) -> Option<String> + Send + Sync>;

pub type AfterAuthenticationVerification =
    Arc<dyn Fn(AfterAuthenticationVerificationInput) + Send + Sync>;

#[derive(Clone)]
pub struct PasskeyRegistrationOptions {
    pub require_session: bool,
    pub resolve_user: Option<ResolveRegistrationUser>,
    pub after_verification: Option<AfterRegistrationVerification>,
    pub extensions: Option<Value>,
}

impl Default for PasskeyRegistrationOptions {
    fn default() -> Self {
        Self {
            require_session: true,
            resolve_user: None,
            after_verification: None,
            extensions: None,
        }
    }
}

impl PasskeyRegistrationOptions {
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn require_session(mut self, require_session: bool) -> Self {
        self.require_session = require_session;
        self
    }

    #[must_use]
    pub fn resolve_user<F>(mut self, resolver: F) -> Self
    where
        F: Fn(ResolveRegistrationUserInput) -> Option<PasskeyRegistrationUser>
            + Send
            + Sync
            + 'static,
    {
        self.resolve_user = Some(Arc::new(resolver));
        self
    }

    #[must_use]
    pub fn after_verification<F>(mut self, callback: F) -> Self
    where
        F: Fn(AfterRegistrationVerificationInput) -> Option<String> + Send + Sync + 'static,
    {
        self.after_verification = Some(Arc::new(callback));
        self
    }

    #[must_use]
    pub fn extensions(mut self, extensions: Value) -> Self {
        self.extensions = Some(extensions);
        self
    }
}

#[derive(Clone, Default)]
pub struct PasskeyAuthenticationOptions {
    pub after_verification: Option<AfterAuthenticationVerification>,
    pub extensions: Option<Value>,
}

impl PasskeyAuthenticationOptions {
    pub fn new() -> Self {
        Self::default()
    }

    #[must_use]
    pub fn after_verification<F>(mut self, callback: F) -> Self
    where
        F: Fn(AfterAuthenticationVerificationInput) + Send + Sync + 'static,
    {
        self.after_verification = Some(Arc::new(callback));
        self
    }

    #[must_use]
    pub fn extensions(mut self, extensions: Value) -> Self {
        self.extensions = Some(extensions);
        self
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolveRegistrationUserInput {
    pub context: Option<String>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct AfterRegistrationVerificationInput {
    pub user: PasskeyRegistrationUser,
    pub client_data: Value,
    pub context: Option<String>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct AfterAuthenticationVerificationInput {
    pub credential_id: String,
    pub client_data: Value,
}