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