1use std::fmt;
2
3use base64::{engine::general_purpose, Engine as _};
4use http::HeaderValue;
5
6use super::{error, Error};
7
8#[derive(Clone, PartialEq, Eq, Hash)]
9pub enum AuthScheme<'a> {
10 Basic {
15 username: &'a str,
16 password: &'a str,
17 },
18
19 Bearer { token: &'a str },
24
25 Digest(DigestBuilder<'a>),
30
31 HOBA {
36 result: String, },
38
39 Mutual { credentials: &'a str },
44
45 Negotiate { token: &'a str },
50
51 Vapid {
56 public_key: &'a str,
57 subject: &'a str,
58 signature: String,
59 },
60
61 Scram {
66 variant: SCRAMVariant,
67 credentials: String,
68 },
69
70 Aws4HmacSha256 {
75 access_key: &'a str,
76 signature: String,
77 region: &'a str,
78 service: &'a str,
79 date: String,
80 },
81
82 Custom {
87 scheme: &'a str,
88 credentials: &'a str,
89 },
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash)]
93pub enum SCRAMVariant {
94 SHA1,
95 SHA256,
96}
97
98impl<'a> AuthScheme<'a> {
99 pub fn basic(username: &'a str, password: &'a str) -> Self {
100 Self::Basic { username, password }
101 }
102
103 pub fn bearer(token: &'a str) -> Self {
104 Self::Bearer { token }
105 }
106
107 pub fn digest(digest: DigestBuilder<'a>) -> Self {
108 Self::Digest(digest)
109 }
110
111 pub fn hoba(result: impl Into<String>) -> Self {
112 Self::HOBA {
113 result: result.into(),
114 }
115 }
116
117 pub fn mutual(credentials: &'a str) -> Self {
118 Self::Mutual { credentials }
119 }
120
121 pub fn negotiate(token: &'a str) -> Self {
122 Self::Negotiate { token }
123 }
124
125 pub fn vapid(public_key: &'a str, subject: &'a str, signature: impl Into<String>) -> Self {
126 Self::Vapid {
127 public_key,
128 subject,
129 signature: signature.into(),
130 }
131 }
132
133 pub fn scram(variant: SCRAMVariant, credentials: impl Into<String>) -> Self {
134 Self::Scram {
135 variant,
136 credentials: credentials.into(),
137 }
138 }
139
140 pub fn aws4_hmac_sha256(
141 access_key: &'a str,
142 signature: impl Into<String>,
143 region: &'a str,
144 service: &'a str,
145 date: impl Into<String>,
146 ) -> Self {
147 Self::Aws4HmacSha256 {
148 access_key,
149 signature: signature.into(),
150 region,
151 service,
152 date: date.into(),
153 }
154 }
155
156 pub fn custom(scheme: &'a str, credentials: &'a str) -> Self {
157 Self::Custom {
158 scheme,
159 credentials,
160 }
161 }
162
163 pub fn to_header_value(self) -> Result<HeaderValue, Error> {
164 let auth_string = match self {
165 AuthScheme::Basic { username, password } => {
166 let credentials = format!("{username}:{password}");
167 let encoded = general_purpose::STANDARD.encode(credentials);
168 format!("Basic {encoded}")
169 }
170 AuthScheme::Bearer { token } => format!("Bearer {token}"),
171 AuthScheme::Digest(digest) => digest.build(),
172 AuthScheme::HOBA { result } => format!("HOBA result=\"{result}\""),
173 AuthScheme::Mutual { credentials } => format!("Mutual {credentials}"),
174 AuthScheme::Negotiate { token } => format!("Negotiate {token}"),
175 AuthScheme::Vapid {
176 public_key,
177 subject,
178 signature,
179 } => format!("VAPID k={public_key}, a={subject}, s={signature}"),
180 AuthScheme::Scram {
181 variant,
182 credentials,
183 } => {
184 let scheme_name = match variant {
185 SCRAMVariant::SHA1 => "SCRAM-SHA-1",
186 SCRAMVariant::SHA256 => "SCRAM-SHA-256",
187 };
188 format!("{scheme_name} {credentials}")
189 }
190 AuthScheme::Aws4HmacSha256 {
191 access_key,
192 signature,
193 region,
194 service,
195 date,
196 } => format!("AWS4-HMAC-SHA256 Credential={access_key}/{date}/{region}/{service}/aws4_request, SignedHeaders=host;x-amz-date, Signature={signature}"),
197 AuthScheme::Custom {
198 scheme,
199 credentials,
200 } =>
201 format!("{scheme} {credentials}"),
202
203 };
204
205 let mut value = HeaderValue::from_str(&auth_string).map_err(|_|error::auth::invalid_scheme(auth_string))?;
206 value.set_sensitive(true);
207 Ok(value)
208 }
209
210 pub fn scheme_name(&self) -> &str {
211 match self {
212 AuthScheme::Basic { .. } => "Basic",
213 AuthScheme::Bearer { .. } => "Bearer",
214 AuthScheme::Digest { .. } => "Digest",
215 AuthScheme::HOBA { .. } => "HOBA",
216 AuthScheme::Mutual { .. } => "Mutual",
217 AuthScheme::Negotiate { .. } => "Negotiate",
218 AuthScheme::Vapid { .. } => "VAPID",
219 AuthScheme::Scram { variant, .. } => match variant {
220 SCRAMVariant::SHA1 => "SCRAM-SHA-1",
221 SCRAMVariant::SHA256 => "SCRAM-SHA-256",
222 },
223 AuthScheme::Aws4HmacSha256 { .. } => "AWS4-HMAC-SHA256",
224 AuthScheme::Custom { scheme, .. } => scheme,
225 }
226 }
227}
228
229impl<'a> fmt::Display for AuthScheme<'a> {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 match self {
232 AuthScheme::Basic { username, .. } => write!(f, "Basic (user: {username})"),
233 AuthScheme::Bearer { .. } => write!(f, "Bearer token"),
234 AuthScheme::Digest(digest) => write!(f, "{digest}"),
235 AuthScheme::HOBA { .. } => write!(f, "HOBA"),
236 AuthScheme::Mutual { .. } => write!(f, "Mutual"),
237 AuthScheme::Negotiate { .. } => write!(f, "Negotiate"),
238 AuthScheme::Vapid { subject, .. } => write!(f, "VAPID ({subject})"),
239 AuthScheme::Scram { variant, .. } => write!(f, "SCRAM-{variant:?}"),
240 AuthScheme::Aws4HmacSha256 {
241 region, service, ..
242 } => write!(f, "AWS4 ({region}/{service})"),
243 AuthScheme::Custom { scheme, .. } => write!(f, "Custom ({scheme})"),
244 }
245 }
246}
247
248impl<'a> fmt::Debug for AuthScheme<'a> {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 match self {
251 AuthScheme::Basic { username, password: _ } => f
252 .debug_struct("Basic")
253 .field("username", username)
254 .field("password", &"[REDACTED]")
255 .finish(),
256
257 AuthScheme::Bearer { token: _ } => f
258 .debug_struct("Bearer")
259 .field("token", &"[REDACTED]")
260 .finish(),
261
262 AuthScheme::Digest(digest) => f
263 .debug_tuple("Digest")
264 .field(digest)
265 .finish(),
266
267 AuthScheme::HOBA { result: _ } => f
268 .debug_struct("HOBA")
269 .field("result", &"[REDACTED]")
270 .finish(),
271
272 AuthScheme::Mutual { credentials: _ } => f
273 .debug_struct("Mutual")
274 .field("credentials", &"[REDACTED]")
275 .finish(),
276
277 AuthScheme::Negotiate { token: _ } => f
278 .debug_struct("Negotiate")
279 .field("token", &"[REDACTED]")
280 .finish(),
281
282 AuthScheme::Vapid {
283 public_key: _,
284 subject,
285 signature: _
286 } => f
287 .debug_struct("Vapid")
288 .field("public_key", &"[REDACTED]")
289 .field("subject", subject)
290 .field("signature", &"[REDACTED]")
291 .finish(),
292
293 AuthScheme::Scram {
294 variant,
295 credentials: _
296 } => f
297 .debug_struct("Scram")
298 .field("variant", variant)
299 .field("credentials", &"[REDACTED]")
300 .finish(),
301
302 AuthScheme::Aws4HmacSha256 {
303 access_key:_,
304 signature: _,
305 region,
306 service,
307 date,
308 } => f
309 .debug_struct("Aws4HmacSha256")
310 .field("access_key", &"[REDACTED]")
311 .field("signature", &"[REDACTED]")
312 .field("region", region)
313 .field("service", service)
314 .field("date", date)
315 .finish(),
316
317 AuthScheme::Custom {
318 scheme,
319 credentials: _
320 } => f
321 .debug_struct("Custom")
322 .field("scheme", scheme)
323 .field("credentials", &"[REDACTED]")
324 .finish(),
325 }
326 }
327}
328
329#[derive(Clone, PartialEq, Eq, Hash)]
330pub struct DigestBuilder<'a> {
331 username: &'a str,
332 realm: &'a str,
333 nonce: &'a str,
334 uri: &'a str,
335 response: &'a str,
336
337 algorithm: Option<&'a str>,
338 cnonce: Option<&'a str>,
339 opaque: Option<&'a str>,
340 qop: Option<&'a str>,
341 nc: Option<&'a str>,
342}
343
344impl<'a> DigestBuilder<'a> {
345 pub fn new(
346 username: &'a str,
347 realm: &'a str,
348 nonce: &'a str,
349 uri: &'a str,
350 response: &'a str,
351 ) -> Self {
352 Self {
353 username,
354 realm,
355 nonce,
356 uri,
357 response,
358 algorithm: None,
359 cnonce: None,
360 opaque: None,
361 qop: None,
362 nc: None,
363 }
364 }
365
366 pub fn algorithm(mut self, algorithm: &'a str) -> Self {
367 self.algorithm = Some(algorithm);
368 self
369 }
370 pub fn cnonce(mut self, cnonce: &'a str) -> Self {
371 self.cnonce = Some(cnonce);
372 self
373 }
374 pub fn opaque(mut self, opaque: &'a str) -> Self {
375 self.opaque = Some(opaque);
376 self
377 }
378 pub fn qop(mut self, qop: &'a str) -> Self {
379 self.qop = Some(qop);
380 self
381 }
382 pub fn nc(mut self, nc: &'a str) -> Self {
383 self.nc = Some(nc);
384 self
385 }
386
387 pub fn build(self) -> String {
388 let Self {
389 username,
390 realm,
391 nonce,
392 uri,
393 response,
394 algorithm,
395 cnonce,
396 opaque,
397 qop,
398 nc,
399 } = self;
400 let mut parts = vec![
401 format!("username=\"{}\"", username),
402 format!("realm=\"{}\"", realm),
403 format!("nonce=\"{}\"", nonce),
404 format!("uri=\"{}\"", uri),
405 format!("response=\"{}\"", response),
406 ];
407
408 if let Some(alg) = algorithm {
409 parts.push(format!("algorithm={alg}"));
410 }
411 if let Some(cn) = cnonce {
412 parts.push(format!("cnonce=\"{cn}\""));
413 }
414 if let Some(op) = opaque {
415 parts.push(format!("opaque=\"{op}\""));
416 }
417 if let Some(q) = qop {
418 parts.push(format!("qop={q}"));
419 }
420 if let Some(n) = nc {
421 parts.push(format!("nc={n}"));
422 }
423
424 format!("Digest {}", parts.join(", "))
425 }
426}
427
428impl fmt::Display for DigestBuilder<'_> {
429 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430 write!(f, "Digest (user: {}, realm: {})", self.username, self.realm)
431 }
432}
433
434impl<'a> fmt::Debug for DigestBuilder<'a> {
435 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436 f.debug_struct("DigestBuilder")
437 .field("username", &self.username)
438 .field("realm", &self.realm)
439 .field("nonce", &"[REDACTED]")
440 .field("uri", &self.uri)
441 .field("response", &"[REDACTED]")
442 .field("algorithm", &self.algorithm)
443 .field("cnonce", &self.cnonce.map(|_| "[REDACTED]"))
444 .field("opaque", &self.opaque.map(|_| "[REDACTED]"))
445 .field("qop", &self.qop)
446 .field("nc", &self.nc)
447 .finish()
448 }
449}