Skip to main content

nextcloud_auth/basic/
basic_auth.rs

1use std::fmt;
2
3use base64::Engine as _;
4use base64::prelude::BASE64_STANDARD;
5use nextcloud_sdk_core::{AuthProvider, Headers};
6
7use super::AppPassword;
8use crate::SecretString;
9
10/// Applies [`AppPassword`] credentials as an `Authorization: Basic` header.
11///
12/// The value is `base64(user:password)`; the colon separator is fixed by
13/// RFC 7617, so neither field may contain one — callers use the URL-encoded
14/// Nextcloud user name, which never does.
15#[derive(Clone)]
16pub struct BasicAuth {
17    credentials: AppPassword,
18}
19
20impl BasicAuth {
21    /// Builds a provider from a user name and its app password.
22    pub fn new(user: impl Into<String>, password: SecretString) -> Self {
23        Self {
24            credentials: AppPassword::new(user, password),
25        }
26    }
27
28    /// Builds a provider from already-assembled credentials.
29    pub const fn from_app_password(credentials: AppPassword) -> Self {
30        Self { credentials }
31    }
32
33    /// The credentials this provider applies.
34    pub const fn credentials(&self) -> &AppPassword {
35        &self.credentials
36    }
37
38    /// The header value sent to the server.
39    pub fn header_value(&self) -> String {
40        let encoded = BASE64_STANDARD.encode(self.encoded_pair());
41        format!("Basic {encoded}")
42    }
43
44    fn encoded_pair(&self) -> Vec<u8> {
45        let mut pair = Vec::with_capacity(self.credentials.user().len() + 1);
46        pair.extend_from_slice(self.credentials.user().as_bytes());
47        pair.push(b':');
48        pair.extend_from_slice(self.credentials.password().expose().as_bytes());
49        pair
50    }
51}
52
53impl AuthProvider for BasicAuth {
54    fn authenticate(&self, headers: &mut Headers) {
55        tracing::trace!(user = %self.credentials.user(), "Applying Basic authentication header");
56        headers.set("Authorization", self.header_value());
57    }
58}
59
60impl fmt::Debug for BasicAuth {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.debug_struct("BasicAuth")
63            .field("credentials", &self.credentials)
64            .finish()
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use base64::Engine as _;
71    use base64::prelude::BASE64_STANDARD;
72    use nextcloud_sdk_core::{AuthProvider, Headers};
73
74    use super::BasicAuth;
75    use crate::SecretString;
76
77    fn basic_auth() -> BasicAuth {
78        BasicAuth::new("alice", SecretString::new("app-password"))
79    }
80
81    #[test]
82    fn header_value_matches_independent_encoding() {
83        let provider = BasicAuth::new("alice", SecretString::new("app-password"));
84        let expected = BASE64_STANDARD.encode(b"alice:app-password");
85        assert_eq!(provider.header_value(), format!("Basic {expected}"));
86    }
87
88    #[test]
89    fn authenticate_sets_the_authorization_header() {
90        let mut headers = Headers::new();
91        basic_auth().authenticate(&mut headers);
92        assert_eq!(
93            headers.get("Authorization"),
94            Some("Basic YWxpY2U6YXBwLXBhc3N3b3Jk")
95        );
96    }
97
98    #[test]
99    fn authenticate_replaces_an_existing_authorization_header() {
100        let mut headers =
101            Headers::new().with(nextcloud_sdk_core::Header::new("authorization", "stale"));
102        basic_auth().authenticate(&mut headers);
103        assert_eq!(headers.len(), 1);
104        assert_eq!(
105            headers.get("Authorization"),
106            Some("Basic YWxpY2U6YXBwLXBhc3N3b3Jk")
107        );
108    }
109
110    #[test]
111    fn debug_redacts_both_the_password_and_the_encoded_pair() {
112        let rendered = format!("{:?}", basic_auth());
113        assert!(!rendered.contains("app-password"));
114        assert!(!rendered.contains("YWxpY2U6YXBwLXBhc3N3b3Jk"));
115        assert!(rendered.contains("<redacted>"));
116    }
117}