Skip to main content

alibi_api/plugins/passkey/
registration.rs

1//! Trusted application policy for passkey-first registration.
2use alibi_core::{AuthConfig, AuthRequest, AuthResult, ContextExtensions, utils::json::JsValue};
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub struct PasskeyRegistrationUser {
10    pub id: String,
11    pub name: String,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub display_name: Option<String>,
14}
15
16/// Immutable request and application settings; captures may hold typed services.
17/// A captured store is not automatically rebound to the registration transaction.
18pub struct PasskeyRegistrationContext<'a> {
19    pub request: &'a AuthRequest,
20    pub auth_config: &'a AuthConfig,
21    pub extensions: &'a ContextExtensions,
22}
23
24impl std::fmt::Debug for PasskeyRegistrationContext<'_> {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.debug_struct("PasskeyRegistrationContext")
27            .finish_non_exhaustive()
28    }
29}
30
31/// Resolve an authorized registration identity when no session is available.
32#[async_trait]
33pub trait PasskeyUserResolver: Send + Sync {
34    async fn resolve_user(
35        &self,
36        context: &PasskeyRegistrationContext<'_>,
37        requested_context: Option<&str>,
38    ) -> AuthResult<Option<PasskeyRegistrationUser>>;
39}
40
41/// Facts supplied after the requested registration protocol and attestation policy
42/// verifies. None attestation admits a credential without proving key possession.
43#[derive(Debug, Clone)]
44pub struct VerifiedPasskeyRegistration {
45    pub credential_id: String,
46    pub public_key: Vec<u8>,
47    pub counter: u64,
48    pub aaguid: Option<String>,
49    pub device_type: String,
50    pub backed_up: bool,
51}
52
53#[derive(Debug, Default)]
54pub struct PasskeyRegistrationOverride {
55    /// Empty values are absent, as with upstream's truthiness check.
56    pub user_id: Option<String>,
57    pub name: Option<String>,
58}
59
60#[async_trait]
61pub trait PasskeyRegistrationAfterVerification: Send + Sync {
62    async fn after_verification(
63        &self,
64        context: &PasskeyRegistrationContext<'_>,
65        verification: &VerifiedPasskeyRegistration,
66        user: &PasskeyRegistrationUser,
67        client_data: &JsValue,
68        stored_context: Option<&str>,
69    ) -> AuthResult<Option<PasskeyRegistrationOverride>>;
70}
71
72#[derive(Clone)]
73pub struct PasskeyRegistrationConfig {
74    pub require_session: bool,
75    pub resolve_user: Option<Arc<dyn PasskeyUserResolver>>,
76    pub after_verification: Option<Arc<dyn PasskeyRegistrationAfterVerification>>,
77}
78impl Default for PasskeyRegistrationConfig {
79    fn default() -> Self {
80        Self {
81            require_session: true,
82            resolve_user: None,
83            after_verification: None,
84        }
85    }
86}
87impl std::fmt::Debug for PasskeyRegistrationConfig {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("PasskeyRegistrationConfig")
90            .field("require_session", &self.require_session)
91            .field("resolve_user", &self.resolve_user.is_some())
92            .field("after_verification", &self.after_verification.is_some())
93            .finish()
94    }
95}
96
97pub(super) fn trim_name(value: &str) -> &str {
98    value.trim_matches(|character| {
99        matches!(character,
100        '\u{0009}'..='\u{000D}' | '\u{0020}' | '\u{00A0}' | '\u{1680}' |
101        '\u{2000}'..='\u{200A}' | '\u{2028}' | '\u{2029}' | '\u{202F}' |
102        '\u{205F}' | '\u{3000}' | '\u{FEFF}')
103    })
104}
105
106/// Domain client errors and explicit public API errors preserve their wire contract.
107pub(super) const fn is_application_error(error: &alibi_core::AuthError) -> bool {
108    matches!(
109        error,
110        alibi_core::AuthError::Upstream { .. } | alibi_core::AuthError::Api { .. }
111    ) || error.status_code() < 500
112}