passkey_authenticator/
user_validation.rs

1use passkey_types::ctap2::{
2    Ctap2Error,
3    make_credential::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity},
4};
5
6use crate::passkey::PasskeyAccessor;
7
8#[cfg(any(test, feature = "testable", doc))]
9use passkey_types::Passkey;
10
11#[cfg(doc)]
12use crate::Authenticator;
13
14/// Additional information that can be displayed to the user if the authenticator has a display.
15#[derive(Debug, Clone, PartialEq)]
16pub enum UiHint<'a, P> {
17    /// Inform the user that the operation cannot be completed because the user already has a credential registered.
18    InformExcludedCredentialFound(&'a P),
19
20    /// Inform the user that the operation cannot be completed because the user has no matching credentials registered.
21    InformNoCredentialsFound,
22
23    /// Request permission to save the credential in this object.
24    RequestNewCredential(
25        &'a PublicKeyCredentialUserEntity,
26        &'a PublicKeyCredentialRpEntity,
27    ),
28
29    /// Request permission to use the existing credential in this object.
30    RequestExistingCredential(&'a P),
31}
32
33/// The result of a user validation check.
34#[derive(Clone, Copy, PartialEq)]
35pub struct UserCheck {
36    /// Indicates whether the user was present.
37    pub presence: bool,
38
39    /// Indicates whether the user was verified.
40    pub verification: bool,
41}
42
43/// Pluggable trait for the [`Authenticator`] to do user interaction and verification.
44#[cfg_attr(any(test, feature = "testable"), mockall::automock(type PasskeyItem = Passkey;))]
45#[async_trait::async_trait]
46pub trait UserValidationMethod {
47    /// The type of the passkey item that can be used to display additional information about the operation to the user.
48    type PasskeyItem: PasskeyAccessor + Send + Sync;
49
50    /// Check for the user's presence and obtain consent for the operation. The operation may
51    /// also require the user to be verified.
52    ///
53    /// * `hint` - Can be used to display additional information about the operation to the user.
54    /// * `presence` - Indicates whether the user's presence is required.
55    /// * `verification` - Indicates whether the user should be verified.
56    async fn check_user<'a>(
57        &self,
58        hint: UiHint<'a, Self::PasskeyItem>,
59        presence: bool,
60        verification: bool,
61    ) -> Result<UserCheck, Ctap2Error>;
62
63    /// Indicates whether this type is capable of testing user presence.
64    fn is_presence_enabled(&self) -> bool;
65
66    /// Indicates that this type is capable of verifying the user within itself.
67    /// For example, devices with UI, biometrics fall into this category.
68    ///
69    /// If `Some(true)`, it indicates that the device is capable of user verification
70    /// within itself and has been configured.
71    ///
72    /// If Some(false), it indicates that the device is capable of user verification
73    /// within itself and has not been yet configured. For example, a biometric device that has not
74    /// yet been configured will return this parameter set to false.
75    ///
76    /// If `None`, it indicates that the device is not capable of user verification within itself.
77    ///
78    /// A device that can only do Client PIN will set this to `None`.
79    ///
80    /// If a device is capable of verifying the user within itself as well as able to do Client PIN,
81    ///  it will return both `Some` and the Client PIN option.
82    fn is_verification_enabled(&self) -> Option<bool>;
83}
84
85/// A version of the [`UiHint`] that uses a [`Passkey`] as the passkey item, is not tied to any specific lifetime,
86/// and does not verify new passkey items which contain new random data that the tests cannot know about beforehand.
87#[cfg(any(test, feature = "testable"))]
88#[derive(Debug, Clone)]
89pub enum MockUiHint {
90    InformExcludedCredentialFound(Passkey),
91    InformNoCredentialsFound,
92    RequestNewCredential(PublicKeyCredentialUserEntity, PublicKeyCredentialRpEntity),
93    RequestExistingCredential(Passkey),
94}
95
96#[cfg(any(test, feature = "testable"))]
97impl MockUserValidationMethod {
98    /// Sets up the mock for returning true for the verification.
99    pub fn verified_user(times: usize) -> Self {
100        let mut user_mock = MockUserValidationMethod::new();
101        user_mock.expect_is_presence_enabled().returning(|| true);
102        user_mock
103            .expect_is_verification_enabled()
104            .returning(|| Some(true))
105            .times(..);
106        user_mock.expect_is_presence_enabled().returning(|| true);
107        user_mock
108            .expect_check_user()
109            .with(
110                mockall::predicate::always(),
111                mockall::predicate::eq(true),
112                mockall::predicate::eq(true),
113            )
114            .returning(|_, _, _| {
115                Ok(UserCheck {
116                    presence: true,
117                    verification: true,
118                })
119            })
120            .times(times);
121        user_mock
122    }
123
124    /// Sets up the mock for returning true for the verification.
125    pub fn verified_user_with_hint(times: usize, expected_hint: MockUiHint) -> Self {
126        let mut user_mock = MockUserValidationMethod::new();
127        user_mock
128            .expect_is_verification_enabled()
129            .returning(|| Some(true))
130            .times(..);
131        user_mock
132            .expect_is_presence_enabled()
133            .returning(|| true)
134            .times(..);
135        user_mock
136            .expect_check_user()
137            .withf(move |actual_hint, presence, verification| {
138                *presence
139                    && *verification
140                    && match &expected_hint {
141                        MockUiHint::InformExcludedCredentialFound(p) => {
142                            actual_hint == &UiHint::InformExcludedCredentialFound(p)
143                        }
144                        MockUiHint::InformNoCredentialsFound => {
145                            matches!(actual_hint, UiHint::InformNoCredentialsFound)
146                        }
147                        MockUiHint::RequestNewCredential(user, rp) => {
148                            actual_hint == &UiHint::RequestNewCredential(user, rp)
149                        }
150                        MockUiHint::RequestExistingCredential(p) => {
151                            actual_hint == &UiHint::RequestExistingCredential(p)
152                        }
153                    }
154            })
155            .returning(|_, _, _| {
156                Ok(UserCheck {
157                    presence: true,
158                    verification: true,
159                })
160            })
161            .times(times);
162        user_mock
163    }
164}