1use std::fmt;
2
3use base64::{Engine as _, engine::general_purpose};
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!(
197 "AWS4-HMAC-SHA256 Credential={access_key}/{date}/{region}/{service}/aws4_request, SignedHeaders=host;x-amz-date, Signature={signature}"
198 ),
199 AuthScheme::Custom {
200 scheme,
201 credentials,
202 } => format!("{scheme} {credentials}"),
203 };
204
205 let mut value = HeaderValue::from_str(&auth_string)
206 .map_err(|_| error::auth::invalid_scheme(auth_string))?;
207 value.set_sensitive(true);
208 Ok(value)
209 }
210
211 pub fn scheme_name(&self) -> &str {
212 match self {
213 AuthScheme::Basic { .. } => "Basic",
214 AuthScheme::Bearer { .. } => "Bearer",
215 AuthScheme::Digest { .. } => "Digest",
216 AuthScheme::HOBA { .. } => "HOBA",
217 AuthScheme::Mutual { .. } => "Mutual",
218 AuthScheme::Negotiate { .. } => "Negotiate",
219 AuthScheme::Vapid { .. } => "VAPID",
220 AuthScheme::Scram { variant, .. } => match variant {
221 SCRAMVariant::SHA1 => "SCRAM-SHA-1",
222 SCRAMVariant::SHA256 => "SCRAM-SHA-256",
223 },
224 AuthScheme::Aws4HmacSha256 { .. } => "AWS4-HMAC-SHA256",
225 AuthScheme::Custom { scheme, .. } => scheme,
226 }
227 }
228}
229
230impl<'a> fmt::Display for AuthScheme<'a> {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 match self {
233 AuthScheme::Basic { username, .. } => write!(f, "Basic (user: {username})"),
234 AuthScheme::Bearer { .. } => write!(f, "Bearer token"),
235 AuthScheme::Digest(digest) => write!(f, "{digest}"),
236 AuthScheme::HOBA { .. } => write!(f, "HOBA"),
237 AuthScheme::Mutual { .. } => write!(f, "Mutual"),
238 AuthScheme::Negotiate { .. } => write!(f, "Negotiate"),
239 AuthScheme::Vapid { subject, .. } => write!(f, "VAPID ({subject})"),
240 AuthScheme::Scram { variant, .. } => write!(f, "SCRAM-{variant:?}"),
241 AuthScheme::Aws4HmacSha256 {
242 region, service, ..
243 } => write!(f, "AWS4 ({region}/{service})"),
244 AuthScheme::Custom { scheme, .. } => write!(f, "Custom ({scheme})"),
245 }
246 }
247}
248
249impl<'a> fmt::Debug for AuthScheme<'a> {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 match self {
252 AuthScheme::Basic {
253 username,
254 password: _,
255 } => f
256 .debug_struct("Basic")
257 .field("username", username)
258 .field("password", &"[REDACTED]")
259 .finish(),
260
261 AuthScheme::Bearer { token: _ } => f
262 .debug_struct("Bearer")
263 .field("token", &"[REDACTED]")
264 .finish(),
265
266 AuthScheme::Digest(digest) => f.debug_tuple("Digest").field(digest).finish(),
267
268 AuthScheme::HOBA { result: _ } => f
269 .debug_struct("HOBA")
270 .field("result", &"[REDACTED]")
271 .finish(),
272
273 AuthScheme::Mutual { credentials: _ } => f
274 .debug_struct("Mutual")
275 .field("credentials", &"[REDACTED]")
276 .finish(),
277
278 AuthScheme::Negotiate { token: _ } => f
279 .debug_struct("Negotiate")
280 .field("token", &"[REDACTED]")
281 .finish(),
282
283 AuthScheme::Vapid {
284 public_key: _,
285 subject,
286 signature: _,
287 } => f
288 .debug_struct("Vapid")
289 .field("public_key", &"[REDACTED]")
290 .field("subject", subject)
291 .field("signature", &"[REDACTED]")
292 .finish(),
293
294 AuthScheme::Scram {
295 variant,
296 credentials: _,
297 } => f
298 .debug_struct("Scram")
299 .field("variant", variant)
300 .field("credentials", &"[REDACTED]")
301 .finish(),
302
303 AuthScheme::Aws4HmacSha256 {
304 access_key: _,
305 signature: _,
306 region,
307 service,
308 date,
309 } => f
310 .debug_struct("Aws4HmacSha256")
311 .field("access_key", &"[REDACTED]")
312 .field("signature", &"[REDACTED]")
313 .field("region", region)
314 .field("service", service)
315 .field("date", date)
316 .finish(),
317
318 AuthScheme::Custom {
319 scheme,
320 credentials: _,
321 } => f
322 .debug_struct("Custom")
323 .field("scheme", scheme)
324 .field("credentials", &"[REDACTED]")
325 .finish(),
326 }
327 }
328}
329
330#[derive(Clone, PartialEq, Eq, Hash)]
331pub struct DigestBuilder<'a> {
332 username: &'a str,
333 realm: &'a str,
334 nonce: &'a str,
335 uri: &'a str,
336 response: &'a str,
337
338 algorithm: Option<&'a str>,
339 cnonce: Option<&'a str>,
340 opaque: Option<&'a str>,
341 qop: Option<&'a str>,
342 nc: Option<&'a str>,
343}
344
345impl<'a> DigestBuilder<'a> {
346 pub fn new(
347 username: &'a str,
348 realm: &'a str,
349 nonce: &'a str,
350 uri: &'a str,
351 response: &'a str,
352 ) -> Self {
353 Self {
354 username,
355 realm,
356 nonce,
357 uri,
358 response,
359 algorithm: None,
360 cnonce: None,
361 opaque: None,
362 qop: None,
363 nc: None,
364 }
365 }
366
367 pub fn algorithm(mut self, algorithm: &'a str) -> Self {
368 self.algorithm = Some(algorithm);
369 self
370 }
371 pub fn cnonce(mut self, cnonce: &'a str) -> Self {
372 self.cnonce = Some(cnonce);
373 self
374 }
375 pub fn opaque(mut self, opaque: &'a str) -> Self {
376 self.opaque = Some(opaque);
377 self
378 }
379 pub fn qop(mut self, qop: &'a str) -> Self {
380 self.qop = Some(qop);
381 self
382 }
383 pub fn nc(mut self, nc: &'a str) -> Self {
384 self.nc = Some(nc);
385 self
386 }
387
388 pub fn build(self) -> String {
389 let Self {
390 username,
391 realm,
392 nonce,
393 uri,
394 response,
395 algorithm,
396 cnonce,
397 opaque,
398 qop,
399 nc,
400 } = self;
401 let mut parts = vec![
402 format!("username=\"{}\"", username),
403 format!("realm=\"{}\"", realm),
404 format!("nonce=\"{}\"", nonce),
405 format!("uri=\"{}\"", uri),
406 format!("response=\"{}\"", response),
407 ];
408
409 if let Some(alg) = algorithm {
410 parts.push(format!("algorithm={alg}"));
411 }
412 if let Some(cn) = cnonce {
413 parts.push(format!("cnonce=\"{cn}\""));
414 }
415 if let Some(op) = opaque {
416 parts.push(format!("opaque=\"{op}\""));
417 }
418 if let Some(q) = qop {
419 parts.push(format!("qop={q}"));
420 }
421 if let Some(n) = nc {
422 parts.push(format!("nc={n}"));
423 }
424
425 format!("Digest {}", parts.join(", "))
426 }
427}
428
429impl fmt::Display for DigestBuilder<'_> {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 write!(f, "Digest (user: {}, realm: {})", self.username, self.realm)
432 }
433}
434
435impl<'a> fmt::Debug for DigestBuilder<'a> {
436 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437 f.debug_struct("DigestBuilder")
438 .field("username", &self.username)
439 .field("realm", &self.realm)
440 .field("nonce", &"[REDACTED]")
441 .field("uri", &self.uri)
442 .field("response", &"[REDACTED]")
443 .field("algorithm", &self.algorithm)
444 .field("cnonce", &self.cnonce.map(|_| "[REDACTED]"))
445 .field("opaque", &self.opaque.map(|_| "[REDACTED]"))
446 .field("qop", &self.qop)
447 .field("nc", &self.nc)
448 .finish()
449 }
450}