Skip to main content

nextcloud_auth/basic/
app_password.rs

1use std::fmt;
2
3use crate::SecretString;
4
5/// A Nextcloud user name paired with an app password.
6///
7/// Nextcloud recommends app passwords over the account password for clients:
8/// they can be revoked individually and never expose the primary login.
9#[derive(Clone)]
10pub struct AppPassword {
11    user: String,
12    password: SecretString,
13}
14
15impl AppPassword {
16    /// Builds credentials from a user name and its app password.
17    pub fn new(user: impl Into<String>, password: SecretString) -> Self {
18        Self {
19            user: user.into(),
20            password,
21        }
22    }
23
24    /// The user name these credentials belong to.
25    pub fn user(&self) -> &str {
26        &self.user
27    }
28
29    /// The app password.
30    ///
31    /// Reading the plaintext is explicit so no accidental `Debug` or log line
32    /// can pick it up.
33    pub fn password(&self) -> &SecretString {
34        &self.password
35    }
36}
37
38impl fmt::Debug for AppPassword {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        f.debug_struct("AppPassword")
41            .field("user", &self.user)
42            .field("password", &self.password)
43            .finish()
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::AppPassword;
50    use crate::SecretString;
51
52    #[test]
53    fn debug_redacts_the_password() {
54        let credentials = AppPassword::new("alice", SecretString::new("s3cr3t-app-password"));
55        let rendered = format!("{credentials:?}");
56        assert!(!rendered.contains("s3cr3t-app-password"));
57        assert!(rendered.contains("<redacted>"));
58        assert!(rendered.contains("alice"));
59    }
60
61    #[test]
62    fn exposes_user_and_password() {
63        let credentials = AppPassword::new("alice", SecretString::new("pw"));
64        assert_eq!(credentials.user(), "alice");
65        assert_eq!(credentials.password().expose(), "pw");
66    }
67}