asknothingx2_util/api/
auth_scheme.rs

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