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