Skip to main content

better_auth_api/plugins/username/
config.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use better_auth_core::{AuthError, AuthResult};
5
6/// Synchronous normalization applied at each upstream endpoint and adapter boundary.
7pub type UsernameNormalizer = dyn Fn(&str) -> AuthResult<String> + Send + Sync;
8
9/// Asynchronous validation of a username or display username.
10#[async_trait]
11pub trait UsernameValidator: Send + Sync {
12    /// Return false to reject the value. Callback errors retain their original response.
13    async fn validate(&self, value: &str) -> AuthResult<bool>;
14}
15
16/// Input normalization for one username field.
17#[derive(Clone, Default)]
18pub enum UsernameNormalization {
19    /// Lowercase usernames and preserve display usernames.
20    #[default]
21    Default,
22    /// Preserve the supplied field exactly.
23    Disabled,
24    /// Run a synchronous, fallible application callback.
25    Custom(Arc<UsernameNormalizer>),
26}
27
28/// Order used by the configured username endpoint or database-hook validator.
29#[derive(Clone, Copy, PartialEq, Eq)]
30pub enum UsernameValidationOrder {
31    /// Validate before normalization, except for the upstream sign-in ordering.
32    PreNormalization,
33    /// Validate after normalization in signup, updates and direct adapter writes.
34    PostNormalization,
35}
36
37/// Username plugin options, independent of the password login plugin.
38#[derive(Clone)]
39pub struct UsernameConfig {
40    /// Minimum UTF-16 length. Zero and NaN select the upstream default of three.
41    pub min_username_length: f64,
42    /// Maximum UTF-16 length. Zero and NaN select the upstream default of thirty.
43    pub max_username_length: f64,
44    /// Custom asynchronous username validator; the default accepts ASCII letters, digits, `_` and `.`.
45    pub username_validator: Option<Arc<dyn UsernameValidator>>,
46    /// Optional asynchronous display-name validator.
47    pub display_username_validator: Option<Arc<dyn UsernameValidator>>,
48    /// Username normalization; default lowercase is separate from disabled normalization.
49    pub username_normalization: UsernameNormalization,
50    /// Display-name normalization; the default preserves input.
51    pub display_username_normalization: UsernameNormalization,
52    /// Preserve omission because upstream sign-in treats it differently from explicit pre-normalization.
53    pub username_validation_order: Option<UsernameValidationOrder>,
54    /// Display-name validation order.
55    pub display_username_validation_order: Option<UsernameValidationOrder>,
56    /// Reject changed usernames through the update-user endpoint once a username is set.
57    pub immutable_username: bool,
58    /// Add and maintain the separate displayUsername field.
59    pub display_username: bool,
60    /// Serialized application field used to store the username.
61    pub username_field_name: Option<String>,
62    /// Serialized application field used to store the display username.
63    pub display_username_field_name: Option<String>,
64}
65
66impl Default for UsernameConfig {
67    fn default() -> Self {
68        Self {
69            min_username_length: 3.0,
70            max_username_length: 30.0,
71            username_validator: None,
72            display_username_validator: None,
73            username_normalization: UsernameNormalization::Default,
74            display_username_normalization: UsernameNormalization::Default,
75            username_validation_order: None,
76            display_username_validation_order: None,
77            immutable_username: false,
78            display_username: true,
79            username_field_name: None,
80            display_username_field_name: None,
81        }
82    }
83}
84
85pub(super) fn error(status: u16, code: &'static str, message: &'static str) -> AuthError {
86    AuthError::Upstream {
87        status,
88        code,
89        message,
90    }
91}
92
93impl UsernameConfig {
94    pub(super) fn normalize(&self, value: &str) -> AuthResult<String> {
95        match &self.username_normalization {
96            UsernameNormalization::Default => Ok(value.to_lowercase()),
97            UsernameNormalization::Disabled => Ok(value.to_owned()),
98            UsernameNormalization::Custom(callback) => callback(value),
99        }
100    }
101
102    pub(super) fn normalize_display(&self, value: &str) -> AuthResult<String> {
103        match &self.display_username_normalization {
104            UsernameNormalization::Default | UsernameNormalization::Disabled => {
105                Ok(value.to_owned())
106            }
107            UsernameNormalization::Custom(callback) => callback(value),
108        }
109    }
110
111    pub(super) async fn validate_raw(
112        &self,
113        value: &str,
114    ) -> AuthResult<Option<(&'static str, &'static str)>> {
115        let length = value.encode_utf16().count() as f64;
116        let minimum = if self.min_username_length == 0.0 || self.min_username_length.is_nan() {
117            3.0
118        } else {
119            self.min_username_length
120        };
121        let maximum = if self.max_username_length == 0.0 || self.max_username_length.is_nan() {
122            30.0
123        } else {
124            self.max_username_length
125        };
126        if length < minimum {
127            return Ok(Some(("USERNAME_TOO_SHORT", "Username is too short")));
128        }
129        if length > maximum {
130            return Ok(Some(("USERNAME_TOO_LONG", "Username is too long")));
131        }
132        let valid = match &self.username_validator {
133            Some(callback) => callback.validate(value).await?,
134            None => {
135                !value.is_empty()
136                    && value
137                        .bytes()
138                        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.'))
139            }
140        };
141        Ok((!valid).then_some(("INVALID_USERNAME", "Username is invalid")))
142    }
143
144    pub(super) async fn validate_input(
145        &self,
146        value: &str,
147    ) -> AuthResult<Option<(&'static str, &'static str)>> {
148        if self.username_validation_order == Some(UsernameValidationOrder::PostNormalization) {
149            self.validate_raw(&self.normalize(value)?).await
150        } else {
151            self.validate_raw(value).await
152        }
153    }
154
155    pub(super) async fn validate_display(&self, value: &str) -> AuthResult<()> {
156        if let Some(callback) = &self.display_username_validator {
157            let valid = if self.display_username_validation_order
158                == Some(UsernameValidationOrder::PostNormalization)
159            {
160                callback.validate(&self.normalize_display(value)?).await?
161            } else {
162                callback.validate(value).await?
163            };
164            if !valid {
165                return Err(error(
166                    400,
167                    "INVALID_DISPLAY_USERNAME",
168                    "Display username is invalid",
169                ));
170            }
171        }
172        Ok(())
173    }
174}