Skip to main content

better_auth_api/plugins/two_factor/
options.rs

1use std::{future::Future, pin::Pin, sync::Arc};
2
3use async_trait::async_trait;
4use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
5use better_auth_core::{AuthResult, SecretKey};
6use rand::{Rng, distributions::Alphanumeric};
7use sha2::{Digest, Sha256};
8
9/// Application hashing callback for stored second-factor codes.
10pub type TwoFactorHasher =
11    Arc<dyn Fn(String) -> Pin<Box<dyn Future<Output = AuthResult<String>> + Send>> + Send + Sync>;
12
13/// Reversible application encryption for OTPs or serialized backup codes.
14#[async_trait]
15pub trait TwoFactorCipher: Send + Sync {
16    /// Encrypt plaintext before persistence.
17    async fn encrypt(&self, plaintext: &str) -> AuthResult<String>;
18    /// Decrypt a stored value before verification.
19    async fn decrypt(&self, ciphertext: &str) -> AuthResult<String>;
20}
21
22/// Storage protection for delivered second-factor OTPs.
23#[derive(Clone, Default)]
24pub enum TwoFactorOtpStorage {
25    /// Store the original code, matching the upstream default.
26    #[default]
27    Plain,
28    /// Store an unpadded base64url SHA-256 digest.
29    Hashed,
30    /// Encrypt using the authentication secret.
31    Encrypted,
32    /// Hash using an asynchronous application callback.
33    CustomHash(TwoFactorHasher),
34    /// Encrypt and decrypt using application callbacks.
35    CustomEncryption(Arc<dyn TwoFactorCipher>),
36}
37
38impl TwoFactorOtpStorage {
39    pub(super) async fn encode(&self, value: &str, secret: SecretKey<'_>) -> AuthResult<String> {
40        match self {
41            Self::Plain => Ok(value.to_owned()),
42            Self::Hashed => Ok(URL_SAFE_NO_PAD.encode(Sha256::digest(value.as_bytes()))),
43            Self::Encrypted => crate::plugins::symmetric::encrypt(secret, value),
44            Self::CustomHash(hash) => hash(value.to_owned()).await,
45            Self::CustomEncryption(cipher) => cipher.encrypt(value).await,
46        }
47    }
48
49    pub(super) async fn verify(
50        &self,
51        stored: &str,
52        input: &str,
53        secret: SecretKey<'_>,
54    ) -> AuthResult<bool> {
55        let (stored, input) = match self {
56            Self::Encrypted => (
57                crate::plugins::symmetric::decrypt(secret, stored)?,
58                input.to_owned(),
59            ),
60            Self::CustomEncryption(cipher) => (cipher.decrypt(stored).await?, input.to_owned()),
61            _ => (stored.to_owned(), self.encode(input, secret).await?),
62        };
63        Ok(stored.len() == input.len() && openssl::memcmp::eq(stored.as_bytes(), input.as_bytes()))
64    }
65}
66
67/// Storage protection for the JSON array of backup codes.
68#[derive(Clone, Default)]
69pub enum BackupCodeStorage {
70    /// Store the serialized JSON array unchanged.
71    Plain,
72    /// Encrypt using the authentication secret, matching the upstream default.
73    #[default]
74    Encrypted,
75    /// Encrypt and decrypt the serialized array using application callbacks.
76    Custom(Arc<dyn TwoFactorCipher>),
77}
78
79impl BackupCodeStorage {
80    pub(super) async fn encode(
81        &self,
82        codes: &[String],
83        secret: SecretKey<'_>,
84    ) -> AuthResult<String> {
85        let json = serde_json::to_string(codes)?;
86        match self {
87            Self::Plain => Ok(json),
88            Self::Encrypted => crate::plugins::symmetric::encrypt(secret, &json),
89            Self::Custom(cipher) => cipher.encrypt(&json).await,
90        }
91    }
92
93    pub(super) async fn decode(
94        &self,
95        stored: &str,
96        secret: SecretKey<'_>,
97    ) -> AuthResult<Option<Vec<String>>> {
98        let json = match self {
99            Self::Plain => stored.to_owned(),
100            Self::Encrypted => crate::plugins::symmetric::decrypt(secret, stored)?,
101            Self::Custom(cipher) => cipher.decrypt(stored).await?,
102        };
103        // Upstream safeJSONParse treats malformed backup-code JSON as an invalid code.
104        Ok(serde_json::from_str(&json).ok())
105    }
106}
107
108/// Recovery-code generation and passwordless management policy.
109#[derive(Clone)]
110pub struct BackupCodeOptions {
111    /// Number of generated codes. Zero produces an empty list.
112    pub amount: usize,
113    /// Number of random characters, before the separator after character five.
114    pub length: usize,
115    /// Replace random generation with application-supplied codes.
116    pub generate: Option<Arc<dyn Fn() -> Vec<String> + Send + Sync>>,
117    /// Stored backup-code representation.
118    pub storage: BackupCodeStorage,
119    /// Override the plugin's passwordless policy for regeneration.
120    pub allow_passwordless: Option<bool>,
121}
122
123impl Default for BackupCodeOptions {
124    fn default() -> Self {
125        Self {
126            amount: 10,
127            length: 10,
128            generate: None,
129            storage: BackupCodeStorage::Encrypted,
130            allow_passwordless: None,
131        }
132    }
133}
134
135impl BackupCodeOptions {
136    pub(super) fn generate(&self) -> Vec<String> {
137        if let Some(generate) = &self.generate {
138            return generate();
139        }
140        (0..self.amount)
141            .map(|_| {
142                let code: String = rand::thread_rng()
143                    .sample_iter(&Alphanumeric)
144                    .take(self.length)
145                    .map(char::from)
146                    .collect();
147                let (prefix, suffix) = code.split_at(code.len().min(5));
148                format!("{prefix}-{suffix}")
149            })
150            .collect()
151    }
152}