asknothingx2_util/api/
auth_scheme.rs

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 authentication - RFC 7617
11    /// Credentials: username:password encoded in base64
12    /// Security: LOW - credentials are easily decoded
13    /// Usage: Simple username/password authentication
14    Basic {
15        username: &'a str,
16        password: &'a str,
17    },
18
19    /// Bearer token authentication - RFC 6750  
20    /// Credentials: Token (usually JWT or OAuth 2.0 access token)
21    /// Security: HIGH - depends on token implementation
22    /// Usage: OAuth 2.0, JWT, API tokens
23    Bearer { token: &'a str },
24
25    /// Digest authentication - RFC 7616
26    /// Credentials: Hashed challenge-response using MD5/SHA
27    /// Security: MEDIUM - prevents password transmission, vulnerable to rainbow tables
28    /// Usage: Enhanced security over Basic auth
29    Digest(DigestBuilder<'a>),
30
31    /// HTTP Origin-Bound Authentication - RFC 7486
32    /// Credentials: Digital signature-based
33    /// Security: HIGH - not vulnerable to phishing attacks
34    /// Usage: Advanced security scenarios, no password storage needed
35    HOBA {
36        result: String, // Contains "kid"."challenge"."nonce"."sig"
37    },
38
39    /// Mutual authentication - RFC 8120
40    /// Credentials: Bidirectional authentication
41    /// Security: HIGH - both client and server authenticate each other
42    /// Usage: High-security environments, certificate-based auth
43    Mutual { credentials: &'a str },
44
45    /// Negotiate/NTLM authentication - RFC 4559
46    /// Credentials: SPNEGO for Kerberos/NTLM
47    /// Security: HIGH - enterprise-grade authentication
48    /// Usage: Windows domain authentication, SSO
49    Negotiate { token: &'a str },
50
51    /// VAPID authentication - RFC 8292
52    /// Credentials: Voluntary Application Server Identification
53    /// Security: MEDIUM - for web push notifications
54    /// Usage: Web push services, contact information verification
55    Vapid {
56        public_key: &'a str,
57        subject: &'a str,
58        signature: String,
59    },
60
61    /// SCRAM authentication - RFC 7804
62    /// Credentials: SASL mechanisms (SHA-1, SHA-256)
63    /// Security: HIGH - salted challenge-response
64    /// Usage: Database authentication, secure challenge-response
65    Scram {
66        variant: SCRAMVariant,
67        credentials: String,
68    },
69
70    /// AWS Signature Version 4 - AWS documentation
71    /// Credentials: HMAC-SHA256 signature
72    /// Security: HIGH - signed requests with access keys
73    /// Usage: AWS API authentication
74    Aws4HmacSha256 {
75        access_key: &'a str,
76        signature: String,
77        region: &'a str,
78        service: &'a str,
79        date: String,
80    },
81
82    /// Custom authentication scheme
83    /// Credentials: User-defined
84    /// Security: Varies
85    /// Usage: Proprietary or non-standard auth schemes
86    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}