1use base64::Engine;
8use http::header::AUTHORIZATION;
9use rskit_errors::{AppError, AppResult, ErrorCode};
10use rskit_security::{BASIC_AUTH_SCHEME, BEARER_AUTH_SCHEME, SecretString};
11use std::fmt;
12
13#[derive(Debug, Clone, Default)]
18#[non_exhaustive]
19pub enum Auth {
20 Bearer(SecretString),
22 Basic {
24 username: String,
26 password: SecretString,
28 },
29 ApiKey {
31 name: String,
33 value: SecretString,
35 },
36 #[default]
38 None,
39}
40
41impl Auth {
42 pub fn bearer(token: impl Into<String>) -> Self {
44 Self::bearer_secret(SecretString::new(token))
45 }
46
47 pub fn bearer_secret(token: SecretString) -> Self {
49 Auth::Bearer(token)
50 }
51
52 pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
54 Self::basic_secret(username, SecretString::new(password))
55 }
56
57 pub fn basic_secret(username: impl Into<String>, password: SecretString) -> Self {
59 Auth::Basic {
60 username: username.into(),
61 password,
62 }
63 }
64
65 pub fn api_key(name: impl Into<String>, value: impl Into<String>) -> Self {
67 Self::api_key_secret(name, SecretString::new(value))
68 }
69
70 pub fn api_key_secret(name: impl Into<String>, value: SecretString) -> Self {
72 Auth::ApiKey {
73 name: name.into(),
74 value,
75 }
76 }
77
78 pub fn header(&self) -> AppResult<Option<(String, String)>> {
83 match self {
84 Auth::Bearer(token) => Ok(Some((
85 AUTHORIZATION.as_str().to_string(),
86 format!("{BEARER_AUTH_SCHEME} {}", token.expose()),
87 ))),
88 Auth::Basic { username, password } => {
89 let credentials = format!("{}:{}", username, password.expose());
90 let encoded = base64::engine::general_purpose::STANDARD.encode(&credentials);
91 Ok(Some((
92 AUTHORIZATION.as_str().to_string(),
93 format!("{BASIC_AUTH_SCHEME} {encoded}"),
94 )))
95 }
96 Auth::ApiKey { name, value } => {
97 if name.parse::<http::HeaderName>().is_err() {
98 return Err(AppError::new(
99 ErrorCode::InvalidInput,
100 format!("invalid API key header name '{name}'"),
101 ));
102 }
103 if value.expose().parse::<http::HeaderValue>().is_err() {
104 return Err(AppError::new(
105 ErrorCode::InvalidInput,
106 format!("invalid API key header value for '{name}'"),
107 ));
108 }
109 Ok(Some((name.clone(), value.expose().to_string())))
110 }
111 Auth::None => Ok(None),
112 }
113 }
114}
115
116impl fmt::Display for Auth {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 match self {
119 Auth::Bearer(_) => write!(f, "{BEARER_AUTH_SCHEME}"),
120 Auth::Basic { .. } => write!(f, "{BASIC_AUTH_SCHEME}"),
121 Auth::ApiKey { name, .. } => write!(f, "ApiKey({})", name),
122 Auth::None => write!(f, "None"),
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn api_key_rejects_invalid_header_value() {
133 let auth = Auth::api_key("x-api-key", "bad\nvalue");
134 assert!(auth.header().is_err());
135 }
136
137 #[test]
138 fn api_key_rejects_invalid_header_name() {
139 let auth = Auth::api_key("bad header", "secret");
140
141 let error = auth.header().expect_err("invalid header name");
142
143 assert_eq!(error.code(), ErrorCode::InvalidInput);
144 assert!(error.message().contains("invalid API key header name"));
145 }
146
147 #[test]
148 fn api_key_and_none_headers_are_explicit() {
149 assert_eq!(
150 Auth::api_key("x-api-key", "secret").header().unwrap(),
151 Some(("x-api-key".to_string(), "secret".to_string()))
152 );
153 assert_eq!(Auth::None.header().unwrap(), None);
154 assert_eq!(Auth::None.to_string(), "None");
155 }
156
157 #[test]
158 fn debug_redacts_secret_values() {
159 let cases = [
160 format!("{:?}", Auth::bearer("secret-token")),
161 format!("{:?}", Auth::basic("user", "secret-password")),
162 format!("{:?}", Auth::api_key("x-api-key", "secret-key")),
163 ];
164
165 for formatted in cases {
166 assert!(formatted.contains("SecretString(***)"));
167 assert!(!formatted.contains("secret-token"));
168 assert!(!formatted.contains("secret-password"));
169 assert!(!formatted.contains("secret-key"));
170 }
171 }
172
173 #[test]
174 fn header_exposes_secret_only_for_request_application() {
175 let auth = Auth::bearer_secret(SecretString::new("secret-token"));
176
177 assert_eq!(
178 auth.header().unwrap(),
179 Some((
180 AUTHORIZATION.as_str().to_string(),
181 format!("{BEARER_AUTH_SCHEME} secret-token")
182 ))
183 );
184 }
185}