nextcloud_auth/basic/
basic_auth.rs1use 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#[derive(Clone)]
16pub struct BasicAuth {
17 credentials: AppPassword,
18}
19
20impl BasicAuth {
21 pub fn new(user: impl Into<String>, password: SecretString) -> Self {
23 Self {
24 credentials: AppPassword::new(user, password),
25 }
26 }
27
28 pub const fn from_app_password(credentials: AppPassword) -> Self {
30 Self { credentials }
31 }
32
33 pub const fn credentials(&self) -> &AppPassword {
35 &self.credentials
36 }
37
38 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}