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}