Skip to main content

asknothingx2_util/api/
auth_scheme.rs

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 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!(
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}