Skip to main content

rustauth_passkey/
options.rs

1use std::future::{ready, Future};
2use std::pin::Pin;
3use std::sync::Arc;
4
5use indexmap::IndexMap;
6use rustauth_core::options::RateLimitRule;
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use time::Duration;
10
11use crate::webauthn::{PasskeyWebAuthnBackend, RealPasskeyWebAuthnBackend};
12
13/// Rate limit settings for passkey ceremony endpoints (challenge generation and verification).
14///
15/// Defaults match RustAuth core's strict sign-in policy (`3` requests per `10` seconds).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct PasskeyRateLimit {
18    pub window: Duration,
19    pub max: u64,
20}
21
22impl Default for PasskeyRateLimit {
23    fn default() -> Self {
24        Self {
25            window: Duration::seconds(10),
26            max: 3,
27        }
28    }
29}
30
31impl PasskeyRateLimit {
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    #[must_use]
37    pub fn window(mut self, window: Duration) -> Self {
38        self.window = window;
39        self
40    }
41
42    #[must_use]
43    pub fn max(mut self, max: u64) -> Self {
44        self.max = max;
45        self
46    }
47}
48
49/// Per signed challenge cookie rate limits for passkey verify endpoints.
50///
51/// Limits verification attempts per challenge independently of the ceremony
52/// IP+path bucket. Storage keys use `HMAC-SHA256(secret, challenge_token)` via
53/// RustAuth core; raw tokens are never persisted.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct PasskeyChallengeRateLimit {
56    pub window: Duration,
57    pub max: u64,
58}
59
60impl Default for PasskeyChallengeRateLimit {
61    fn default() -> Self {
62        Self {
63            window: Duration::minutes(5),
64            max: 5,
65        }
66    }
67}
68
69impl PasskeyChallengeRateLimit {
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    #[must_use]
75    pub fn window(mut self, window: Duration) -> Self {
76        self.window = window;
77        self
78    }
79
80    #[must_use]
81    pub fn max(mut self, max: u64) -> Self {
82        self.max = max;
83        self
84    }
85
86    /// Disable per-challenge verification rate limiting.
87    #[must_use]
88    pub fn disabled(mut self) -> Self {
89        self.max = 0;
90        self
91    }
92
93    pub(crate) fn rule(&self) -> Option<RateLimitRule> {
94        if self.max == 0 || self.window.is_zero() {
95            return None;
96        }
97        Some(RateLimitRule {
98            window: self.window,
99            max: self.max,
100        })
101    }
102}
103
104/// Advanced passkey plugin settings.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct PasskeyAdvancedOptions {
107    pub webauthn_challenge_cookie: String,
108}
109
110impl Default for PasskeyAdvancedOptions {
111    fn default() -> Self {
112        Self {
113            webauthn_challenge_cookie: "better-auth-passkey".to_owned(),
114        }
115    }
116}
117
118/// Passkey management mutation settings (delete, rename).
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct PasskeyManagementOptions {
121    /// Require a fresh session before passkey management mutations.
122    pub require_fresh_session: bool,
123}
124
125impl Default for PasskeyManagementOptions {
126    fn default() -> Self {
127        Self {
128            require_fresh_session: true,
129        }
130    }
131}
132
133impl PasskeyManagementOptions {
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    #[must_use]
139    pub fn require_fresh_session(mut self, require_fresh_session: bool) -> Self {
140        self.require_fresh_session = require_fresh_session;
141        self
142    }
143}
144
145/// Database schema naming overrides for the passkey model.
146///
147/// The Rust API continues to use RustAuth's logical snake_case names; these
148/// overrides only affect the physical table and column names used by adapters.
149#[derive(Debug, Clone, Default, PartialEq, Eq)]
150pub struct PasskeySchemaOptions {
151    pub table_name: Option<String>,
152    pub field_names: IndexMap<String, String>,
153}
154
155impl PasskeySchemaOptions {
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    #[must_use]
161    pub fn table_name(mut self, table_name: impl Into<String>) -> Self {
162        self.table_name = Some(table_name.into());
163        self
164    }
165
166    #[must_use]
167    pub fn field_name(
168        mut self,
169        logical_name: impl Into<String>,
170        database_name: impl Into<String>,
171    ) -> Self {
172        self.field_names
173            .insert(logical_name.into(), database_name.into());
174        self
175    }
176
177    pub(crate) fn table_name_or<'a>(&'a self, default_name: &'a str) -> &'a str {
178        self.table_name.as_deref().unwrap_or(default_name)
179    }
180
181    pub(crate) fn field_name_or<'a>(
182        &'a self,
183        logical_name: &str,
184        default_name: &'a str,
185    ) -> &'a str {
186        self.field_names
187            .get(logical_name)
188            .map(String::as_str)
189            .unwrap_or(default_name)
190    }
191}
192
193/// Passkey plugin settings.
194#[derive(Clone)]
195pub struct PasskeyOptions {
196    pub rp_id: Option<String>,
197    pub rp_name: Option<String>,
198    pub origin: Vec<String>,
199    pub passkey_table: String,
200    pub schema: PasskeySchemaOptions,
201    pub authenticator_selection: AuthenticatorSelection,
202    pub registration: PasskeyRegistrationOptions,
203    pub authentication: PasskeyAuthenticationOptions,
204    pub management: PasskeyManagementOptions,
205    pub advanced: PasskeyAdvancedOptions,
206    pub rate_limit: PasskeyRateLimit,
207    pub challenge_rate_limit: PasskeyChallengeRateLimit,
208    pub(crate) backend: Arc<dyn PasskeyWebAuthnBackend>,
209}
210
211impl Default for PasskeyOptions {
212    fn default() -> Self {
213        Self {
214            rp_id: None,
215            rp_name: None,
216            origin: Vec::new(),
217            passkey_table: "passkeys".to_owned(),
218            schema: PasskeySchemaOptions::default(),
219            authenticator_selection: AuthenticatorSelection::default(),
220            registration: PasskeyRegistrationOptions::default(),
221            authentication: PasskeyAuthenticationOptions::default(),
222            management: PasskeyManagementOptions::default(),
223            advanced: PasskeyAdvancedOptions::default(),
224            rate_limit: PasskeyRateLimit::default(),
225            challenge_rate_limit: PasskeyChallengeRateLimit::default(),
226            backend: Arc::new(RealPasskeyWebAuthnBackend),
227        }
228    }
229}
230
231impl PasskeyOptions {
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    #[must_use]
237    pub fn rp_id(mut self, rp_id: impl Into<String>) -> Self {
238        self.rp_id = Some(rp_id.into());
239        self
240    }
241
242    #[must_use]
243    pub fn rp_name(mut self, rp_name: impl Into<String>) -> Self {
244        self.rp_name = Some(rp_name.into());
245        self
246    }
247
248    #[must_use]
249    pub fn origin(mut self, origin: impl Into<String>) -> Self {
250        self.origin.push(origin.into());
251        self
252    }
253
254    #[must_use]
255    pub fn passkey_table(mut self, table: impl Into<String>) -> Self {
256        self.passkey_table = table.into();
257        self
258    }
259
260    #[must_use]
261    pub fn schema(mut self, schema: PasskeySchemaOptions) -> Self {
262        self.schema = schema;
263        self
264    }
265
266    #[must_use]
267    pub fn authenticator_selection(mut self, selection: AuthenticatorSelection) -> Self {
268        self.authenticator_selection = selection;
269        self
270    }
271
272    #[must_use]
273    pub fn registration(mut self, registration: PasskeyRegistrationOptions) -> Self {
274        self.registration = registration;
275        self
276    }
277
278    #[must_use]
279    pub fn authentication(mut self, authentication: PasskeyAuthenticationOptions) -> Self {
280        self.authentication = authentication;
281        self
282    }
283
284    #[must_use]
285    pub fn management(mut self, management: PasskeyManagementOptions) -> Self {
286        self.management = management;
287        self
288    }
289
290    #[must_use]
291    pub fn advanced(mut self, advanced: PasskeyAdvancedOptions) -> Self {
292        self.advanced = advanced;
293        self
294    }
295
296    #[must_use]
297    pub fn rate_limit(mut self, rate_limit: PasskeyRateLimit) -> Self {
298        self.rate_limit = rate_limit;
299        self
300    }
301
302    #[must_use]
303    pub fn challenge_rate_limit(mut self, challenge_rate_limit: PasskeyChallengeRateLimit) -> Self {
304        self.challenge_rate_limit = challenge_rate_limit;
305        self
306    }
307
308    #[cfg(feature = "test-util")]
309    /// Inject a custom WebAuthn backend (test builds only).
310    #[must_use]
311    pub fn backend(mut self, backend: Arc<dyn PasskeyWebAuthnBackend>) -> Self {
312        self.backend = backend;
313        self
314    }
315
316    pub(crate) fn rate_limit_rule(&self) -> RateLimitRule {
317        RateLimitRule {
318            window: self.rate_limit.window,
319            max: self.rate_limit.max,
320        }
321    }
322}
323
324/// Browser authenticator attachment hint.
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
326pub enum AuthenticatorAttachment {
327    Platform,
328    CrossPlatform,
329}
330
331impl AuthenticatorAttachment {
332    pub(crate) fn from_query(value: &str) -> Option<Self> {
333        match value {
334            "platform" => Some(Self::Platform),
335            "cross-platform" => Some(Self::CrossPlatform),
336            _ => None,
337        }
338    }
339
340    pub(crate) fn as_str(self) -> &'static str {
341        match self {
342            Self::Platform => "platform",
343            Self::CrossPlatform => "cross-platform",
344        }
345    }
346}
347
348/// Resident key preference used in registration options.
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
350pub enum ResidentKeyRequirement {
351    Discouraged,
352    Preferred,
353    Required,
354}
355
356impl ResidentKeyRequirement {
357    pub(crate) fn as_str(self) -> &'static str {
358        match self {
359            Self::Discouraged => "discouraged",
360            Self::Preferred => "preferred",
361            Self::Required => "required",
362        }
363    }
364}
365
366/// User verification preference used in WebAuthn options.
367#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
368pub enum UserVerificationRequirement {
369    Discouraged,
370    Preferred,
371    Required,
372}
373
374impl UserVerificationRequirement {
375    pub(crate) fn as_str(self) -> &'static str {
376        match self {
377            Self::Discouraged => "discouraged",
378            Self::Preferred => "preferred",
379            Self::Required => "required",
380        }
381    }
382}
383
384/// Authenticator selection hints for generated registration options.
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
386pub struct AuthenticatorSelection {
387    pub resident_key: ResidentKeyRequirement,
388    pub user_verification: UserVerificationRequirement,
389    pub authenticator_attachment: Option<AuthenticatorAttachment>,
390}
391
392impl Default for AuthenticatorSelection {
393    fn default() -> Self {
394        Self {
395            resident_key: ResidentKeyRequirement::Preferred,
396            user_verification: UserVerificationRequirement::Preferred,
397            authenticator_attachment: None,
398        }
399    }
400}
401
402impl AuthenticatorSelection {
403    pub fn new() -> Self {
404        Self::default()
405    }
406
407    #[must_use]
408    pub fn resident_key(mut self, resident_key: ResidentKeyRequirement) -> Self {
409        self.resident_key = resident_key;
410        self
411    }
412
413    #[must_use]
414    pub fn user_verification(mut self, user_verification: UserVerificationRequirement) -> Self {
415        self.user_verification = user_verification;
416        self
417    }
418
419    #[must_use]
420    pub fn authenticator_attachment(mut self, attachment: AuthenticatorAttachment) -> Self {
421        self.authenticator_attachment = Some(attachment);
422        self
423    }
424
425    pub(crate) fn with_attachment_override(
426        &self,
427        attachment: Option<AuthenticatorAttachment>,
428    ) -> Self {
429        let mut selection = self.clone();
430        if attachment.is_some() {
431            selection.authenticator_attachment = attachment;
432        }
433        selection
434    }
435
436    pub fn to_json(&self) -> Value {
437        let mut value = json!({
438            "residentKey": self.resident_key.as_str(),
439            "userVerification": self.user_verification.as_str(),
440        });
441        if let Some(attachment) = self.authenticator_attachment {
442            value["authenticatorAttachment"] = json!(attachment.as_str());
443        }
444        value
445    }
446}
447
448/// WebAuthn option customizations resolved for one registration request.
449#[derive(Debug, Clone, PartialEq)]
450pub struct RegistrationWebAuthnOptions {
451    pub authenticator_selection: AuthenticatorSelection,
452    pub extensions: Option<Value>,
453}
454
455impl RegistrationWebAuthnOptions {
456    pub(crate) fn new(
457        authenticator_selection: AuthenticatorSelection,
458        extensions: Option<Value>,
459    ) -> Self {
460        Self {
461            authenticator_selection,
462            extensions,
463        }
464    }
465}
466
467/// User identity used for passkey registration.
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct PasskeyRegistrationUser {
470    pub id: String,
471    pub name: String,
472    pub display_name: Option<String>,
473}
474
475impl PasskeyRegistrationUser {
476    pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
477        Self {
478            id: id.into(),
479            name: name.into(),
480            display_name: None,
481        }
482    }
483
484    #[must_use]
485    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
486        self.display_name = Some(display_name.into());
487        self
488    }
489}
490
491pub type PasskeyBoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
492
493pub type ResolveRegistrationUser = Arc<
494    dyn Fn(ResolveRegistrationUserInput) -> PasskeyBoxFuture<Option<PasskeyRegistrationUser>>
495        + Send
496        + Sync,
497>;
498
499pub type AfterRegistrationVerification = Arc<
500    dyn Fn(AfterRegistrationVerificationInput) -> PasskeyBoxFuture<Option<String>> + Send + Sync,
501>;
502
503/// Rejection returned by authentication `after_verification` hooks to abort login
504/// after WebAuthn proof verification without updating the passkey counter or
505/// minting a session.
506#[derive(Debug, Clone, Copy, PartialEq, Eq)]
507pub struct PasskeyAuthenticationRejected;
508
509pub type AfterAuthenticationVerification = Arc<
510    dyn Fn(
511            AfterAuthenticationVerificationInput,
512        ) -> PasskeyBoxFuture<Result<(), PasskeyAuthenticationRejected>>
513        + Send
514        + Sync,
515>;
516
517pub type PasskeyExtensionsResolver =
518    Arc<dyn Fn(PasskeyExtensionsInput) -> PasskeyBoxFuture<Option<Value>> + Send + Sync>;
519
520#[derive(Clone)]
521pub struct PasskeyRegistrationOptions {
522    pub require_session: bool,
523    pub resolve_user: Option<ResolveRegistrationUser>,
524    pub after_verification: Option<AfterRegistrationVerification>,
525    pub extensions: Option<PasskeyExtensionsResolver>,
526}
527
528impl Default for PasskeyRegistrationOptions {
529    fn default() -> Self {
530        Self {
531            require_session: true,
532            resolve_user: None,
533            after_verification: None,
534            extensions: None,
535        }
536    }
537}
538
539impl PasskeyRegistrationOptions {
540    pub fn new() -> Self {
541        Self::default()
542    }
543
544    #[must_use]
545    pub fn require_session(mut self, require_session: bool) -> Self {
546        self.require_session = require_session;
547        self
548    }
549
550    #[must_use]
551    pub fn resolve_user<F>(mut self, resolver: F) -> Self
552    where
553        F: Fn(ResolveRegistrationUserInput) -> Option<PasskeyRegistrationUser>
554            + Send
555            + Sync
556            + 'static,
557    {
558        self.resolve_user = Some(Arc::new(move |input| Box::pin(ready(resolver(input)))));
559        self
560    }
561
562    #[must_use]
563    pub fn resolve_user_async<F, Fut>(mut self, resolver: F) -> Self
564    where
565        F: Fn(ResolveRegistrationUserInput) -> Fut + Send + Sync + 'static,
566        Fut: Future<Output = Option<PasskeyRegistrationUser>> + Send + 'static,
567    {
568        self.resolve_user = Some(Arc::new(move |input| Box::pin(resolver(input))));
569        self
570    }
571
572    #[must_use]
573    pub fn after_verification<F>(mut self, callback: F) -> Self
574    where
575        F: Fn(AfterRegistrationVerificationInput) -> Option<String> + Send + Sync + 'static,
576    {
577        self.after_verification = Some(Arc::new(move |input| Box::pin(ready(callback(input)))));
578        self
579    }
580
581    #[must_use]
582    pub fn after_verification_async<F, Fut>(mut self, callback: F) -> Self
583    where
584        F: Fn(AfterRegistrationVerificationInput) -> Fut + Send + Sync + 'static,
585        Fut: Future<Output = Option<String>> + Send + 'static,
586    {
587        self.after_verification = Some(Arc::new(move |input| Box::pin(callback(input))));
588        self
589    }
590
591    #[must_use]
592    pub fn extensions(mut self, extensions: Value) -> Self {
593        self.extensions = Some(Arc::new(move |_| Box::pin(ready(Some(extensions.clone())))));
594        self
595    }
596
597    #[must_use]
598    pub fn extensions_resolver<F, Fut>(mut self, resolver: F) -> Self
599    where
600        F: Fn(PasskeyExtensionsInput) -> Fut + Send + Sync + 'static,
601        Fut: Future<Output = Option<Value>> + Send + 'static,
602    {
603        self.extensions = Some(Arc::new(move |input| Box::pin(resolver(input))));
604        self
605    }
606}
607
608#[derive(Clone, Default)]
609pub struct PasskeyAuthenticationOptions {
610    pub after_verification: Option<AfterAuthenticationVerification>,
611    pub extensions: Option<PasskeyExtensionsResolver>,
612}
613
614impl PasskeyAuthenticationOptions {
615    pub fn new() -> Self {
616        Self::default()
617    }
618
619    #[must_use]
620    pub fn after_verification<F>(mut self, callback: F) -> Self
621    where
622        F: Fn(AfterAuthenticationVerificationInput) + Send + Sync + 'static,
623    {
624        self.after_verification = Some(Arc::new(move |input| {
625            callback(input);
626            Box::pin(ready(Ok(())))
627        }));
628        self
629    }
630
631    #[must_use]
632    pub fn after_verification_async<F, Fut>(mut self, callback: F) -> Self
633    where
634        F: Fn(AfterAuthenticationVerificationInput) -> Fut + Send + Sync + 'static,
635        Fut: Future<Output = Result<(), PasskeyAuthenticationRejected>> + Send + 'static,
636    {
637        self.after_verification = Some(Arc::new(move |input| Box::pin(callback(input))));
638        self
639    }
640
641    #[must_use]
642    pub fn extensions(mut self, extensions: Value) -> Self {
643        self.extensions = Some(Arc::new(move |_| Box::pin(ready(Some(extensions.clone())))));
644        self
645    }
646
647    #[must_use]
648    pub fn extensions_resolver<F, Fut>(mut self, resolver: F) -> Self
649    where
650        F: Fn(PasskeyExtensionsInput) -> Fut + Send + Sync + 'static,
651        Fut: Future<Output = Option<Value>> + Send + 'static,
652    {
653        self.extensions = Some(Arc::new(move |input| Box::pin(resolver(input))));
654        self
655    }
656}
657
658#[derive(Debug, Clone, PartialEq, Eq)]
659pub struct ResolveRegistrationUserInput {
660    pub context: Option<String>,
661}
662
663#[derive(Debug, Clone, PartialEq, Eq)]
664pub struct PasskeyExtensionsInput {
665    pub context: Option<String>,
666    /// Authenticated user id when generating session-scoped authentication options.
667    pub user_id: Option<String>,
668}
669
670#[derive(Debug, Clone, PartialEq)]
671pub struct AfterRegistrationVerificationInput {
672    pub user: PasskeyRegistrationUser,
673    pub client_data: Value,
674    pub context: Option<String>,
675}
676
677#[derive(Debug, Clone, PartialEq)]
678pub struct AfterAuthenticationVerificationInput {
679    pub credential_id: String,
680    pub client_data: Value,
681}