Skip to main content

parse_net/
auth.rs

1//! `Authorization`, `Proxy-Authorization`, `WWW-Authenticate`, and
2//! `Proxy-Authenticate` (RFC 9110 section 11), with the `Bearer` (RFC 6750)
3//! and `Basic` (RFC 7617) payloads.
4//!
5//! Credentials are secrets: `Debug` on [`Credentials`] and [`AuthPayload`]
6//! prints the scheme and parameter names only.
7//!
8//! # Examples
9//!
10//! ```
11//! use parse_net::{self as pn, auth, Limits};
12//!
13//! assert_eq!(pn::bearer(b"bearer  mF_9.B5f-4.1JqM==")?, Some(&b"mF_9.B5f-4.1JqM=="[..]));
14//! assert_eq!(pn::bearer(b"Basic dXNlcjpwYXNz")?, None);
15//! assert_eq!(pn::basic(b"Basic dXNlcjpwYXNz")?, Some(&b"dXNlcjpwYXNz"[..]));
16//!
17//! let c = auth::parse_credentials(b"Digest username=\"a\", realm=\"r\"", &Limits::DEFAULT)?;
18//! assert!(c.scheme_is("digest"));
19//! assert_eq!(format!("{c:?}"), "Credentials { scheme: \"Digest\", .. }");
20//! # Ok::<(), pn::ParseError>(())
21//! ```
22
23use core::{fmt, iter::Peekable};
24
25use crate::{
26    class,
27    error::{ErrorKind, ParseError, Result, WriteError, err},
28    limits::Limits,
29    list,
30    scan::control_kind,
31    text::{Word, is_token, parse_word, skip_ows, trim_ows},
32    write::{CountSink, Sink, reserve, unique_names, write_quoted, write_word},
33};
34
35/// Whether `s` is a `token68`: `1*( ALPHA / DIGIT / "-" / "." / "_" / "~" /
36/// "+" / "/" ) *"="`.
37fn is_token68(s: &[u8]) -> bool {
38    let body = s.iter().take_while(|&&b| class::is_token68_char(b)).count();
39    body > 0
40        && s.get(body..)
41            .is_some_and(|pad| pad.iter().all(|&b| b == b'='))
42}
43
44/// One `auth-param`: `token BWS "=" BWS ( token / quoted-string )`.
45fn auth_param(e: &[u8]) -> Result<(&[u8], Word<'_>)> {
46    let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
47    if n == 0 {
48        return Err(refused(e, 0, ErrorKind::NotToken));
49    }
50    let name = e.get(..n).unwrap_or(&[]);
51    let mut i = n + skip_ows(e.get(n..).unwrap_or(&[]));
52    if e.get(i) != Some(&b'=') {
53        return Err(refused(e, i, ErrorKind::MissingValue));
54    }
55    i += 1;
56    i += skip_ows(e.get(i..).unwrap_or(&[]));
57    let (w, used) = parse_word(e.get(i..).unwrap_or(&[])).map_err(|x| match x.kind() {
58        ErrorKind::Empty => refused(e, i, ErrorKind::MissingValue),
59        _ => x.shifted(i),
60    })?;
61    if i + used != e.len() {
62        return Err(refused(e, i + used, ErrorKind::TrailingData));
63    }
64    Ok((name, w))
65}
66
67fn refused(e: &[u8], at: usize, kind: ErrorKind) -> ParseError {
68    match e.get(at) {
69        Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
70        _ => ParseError::new(kind, at),
71    }
72}
73
74/// Whether element `e` is an `auth-param` (a token, optional whitespace,
75/// then `=`), as opposed to the start of a challenge.
76fn is_param_start(e: &[u8]) -> bool {
77    let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
78    n > 0 && e.get(n + skip_ows(e.get(n..).unwrap_or(&[]))) == Some(&b'=')
79}
80
81/// Comma-separated `auth-param`s, validated.
82///
83/// `Debug` prints the parameter names only: values can be secrets.
84#[derive(Clone, Copy, PartialEq, Eq, Hash)]
85pub struct AuthParams<'a> {
86    text: &'a [u8],
87    count: usize,
88}
89
90impl<'a> AuthParams<'a> {
91    /// The parameters, in order.
92    pub fn iter(&self) -> impl Iterator<Item = (&'a [u8], Word<'a>)> + use<'a> {
93        list::parse(self.text, &Limits::DEFAULT.max_list_members(u16::MAX))
94            .into_iter()
95            .flat_map(|l| l.into_iter())
96            .filter_map(|e| auth_param(e).ok())
97    }
98
99    /// The value of the parameter `name` (ASCII case-insensitive).
100    #[must_use]
101    pub fn get(&self, name: &str) -> Option<Word<'a>> {
102        self.iter()
103            .find(|(n, _)| n.eq_ignore_ascii_case(name.as_bytes()))
104            .map(|(_, v)| v)
105    }
106
107    /// The number of parameters.
108    #[must_use]
109    pub const fn len(&self) -> usize {
110        self.count
111    }
112
113    /// Whether there are no parameters.
114    #[must_use]
115    pub const fn is_empty(&self) -> bool {
116        self.count == 0
117    }
118}
119
120impl fmt::Debug for AuthParams<'_> {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.write_str("AuthParams(")?;
123        f.debug_list()
124            .entries(
125                self.iter()
126                    .map(|(n, _)| core::str::from_utf8(n).unwrap_or("?")),
127            )
128            .finish()?;
129        f.write_str(")")
130    }
131}
132
133/// What follows the scheme of a credential or a challenge.
134#[derive(Clone, Copy, PartialEq, Eq, Hash)]
135pub enum AuthPayload<'a> {
136    /// Nothing.
137    None,
138    /// A `token68`, such as a bearer token or `Basic` base64 text.
139    Token68(&'a [u8]),
140    /// `auth-param`s.
141    Params(AuthParams<'a>),
142}
143
144impl fmt::Debug for AuthPayload<'_> {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        match self {
147            AuthPayload::None => f.write_str("None"),
148            AuthPayload::Token68(_) => f.write_str("Token68(..)"),
149            AuthPayload::Params(p) => fmt::Debug::fmt(p, f),
150        }
151    }
152}
153
154/// Validated credentials (`Authorization`, `Proxy-Authorization`).
155///
156/// `Debug` prints the scheme only.
157#[derive(Clone, Copy, PartialEq, Eq, Hash)]
158pub struct Credentials<'a> {
159    scheme: &'a [u8],
160    payload: AuthPayload<'a>,
161}
162
163impl<'a> Credentials<'a> {
164    /// The scheme, as sent.
165    #[must_use]
166    pub const fn scheme(&self) -> &'a [u8] {
167        self.scheme
168    }
169
170    /// Whether the scheme is `s` (ASCII case-insensitive).
171    #[must_use]
172    pub fn scheme_is(&self, s: &str) -> bool {
173        self.scheme.eq_ignore_ascii_case(s.as_bytes())
174    }
175
176    /// The payload.
177    #[must_use]
178    pub const fn payload(&self) -> AuthPayload<'a> {
179        self.payload
180    }
181}
182
183impl fmt::Debug for Credentials<'_> {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.debug_struct("Credentials")
186            .field("scheme", &core::str::from_utf8(self.scheme).unwrap_or("?"))
187            .finish_non_exhaustive()
188    }
189}
190
191/// Validates the `auth-param` list `text` (from `at` in the field) with no
192/// repeated name, and counts it.
193fn check_params(text: &[u8], at: usize, limits: &Limits) -> Result<usize> {
194    let l = list::parse(text, limits).map_err(|e| e.shifted(at))?;
195    let mut count = 0usize;
196    for e in l.iter() {
197        let off = at + (e.as_ptr() as usize).saturating_sub(text.as_ptr() as usize);
198        let (name, _) = auth_param(e).map_err(|x| x.shifted(off))?;
199        // A repeated name within one credential or challenge is refused
200        // (RFC 9110 section 11.2); bounded by `max_params`.
201        let earlier = text
202            .get(..(e.as_ptr() as usize).saturating_sub(text.as_ptr() as usize))
203            .unwrap_or(&[]);
204        let seen = list::parse(earlier, limits)
205            .into_iter()
206            .flat_map(|x| x.into_iter())
207            .filter_map(|x| auth_param(x).ok())
208            .any(|(n, _)| n.eq_ignore_ascii_case(name));
209        if seen {
210            return err(ErrorKind::DuplicateParameter, off);
211        }
212        count += 1;
213        if count > usize::from(limits.params) {
214            return err(ErrorKind::TooManyMembers, off);
215        }
216    }
217    Ok(count)
218}
219
220/// Parses `credentials = auth-scheme [ 1*SP ( token68 / #auth-param ) ]`.
221///
222/// One credential per field; parameter names may not repeat; OWS around
223/// `=` in a parameter is accepted (BWS).
224///
225/// # Errors
226///
227/// [`ErrorKind::NotToken`] for the scheme or a parameter name;
228/// [`ErrorKind::DuplicateParameter`]; [`ErrorKind::MissingValue`];
229/// [`ErrorKind::TrailingData`] (for example a second credential); the
230/// quoted-string and list errors.
231///
232/// # Examples
233///
234/// ```
235/// use parse_net::{auth, Limits};
236///
237/// assert!(auth::parse_credentials(b"Digest a=b, a=c", &Limits::DEFAULT).is_err());
238/// ```
239pub fn parse_credentials<'a>(value: &'a [u8], limits: &Limits) -> Result<Credentials<'a>> {
240    let lead = skip_ows(value);
241    let v = trim_ows(value);
242    let n = v.iter().take_while(|&&b| class::is_tchar(b)).count();
243    if n == 0 {
244        return Err(refused(v, 0, ErrorKind::NotToken).shifted(lead));
245    }
246    let scheme = v.get(..n).unwrap_or(&[]);
247    let rest = v.get(n..).unwrap_or(&[]);
248    if rest.is_empty() {
249        return Ok(Credentials {
250            scheme,
251            payload: AuthPayload::None,
252        });
253    }
254    let spaces = rest.iter().take_while(|&&b| b == b' ').count();
255    if spaces == 0 {
256        return Err(refused(v, n, ErrorKind::NotToken).shifted(lead));
257    }
258    let at = n + spaces;
259    let body = v.get(at..).unwrap_or(&[]);
260    if is_token68(body) {
261        return Ok(Credentials {
262            scheme,
263            payload: AuthPayload::Token68(body),
264        });
265    }
266    if !is_param_start(body) {
267        // A token68 followed by more, or a second scheme.
268        let t68 = body
269            .iter()
270            .take_while(|&&b| class::is_token68_char(b) || b == b'=')
271            .count();
272        return Err(refused(v, at + t68, ErrorKind::TrailingData).shifted(lead));
273    }
274    let count = check_params(body, lead + at, limits)?;
275    Ok(Credentials {
276        scheme,
277        payload: AuthPayload::Params(AuthParams { text: body, count }),
278    })
279}
280
281/// The `Bearer` token of an `Authorization` value (RFC 6750 section 2.1):
282/// `Some(token)` when the scheme is `Bearer` (case-insensitive), `None` for
283/// another scheme.
284///
285/// # Errors
286///
287/// [`ErrorKind::Empty`] for `Bearer` alone; [`ErrorKind::InvalidByte`] when
288/// the payload is not a `b64token`; [`ErrorKind::TrailingData`]; the scheme
289/// errors.
290pub fn bearer(value: &[u8]) -> Result<Option<&[u8]>> {
291    let c = parse_credentials(value, &Limits::DEFAULT)?;
292    if !c.scheme_is("bearer") {
293        return Ok(None);
294    }
295    match c.payload {
296        AuthPayload::Token68(t) => Ok(Some(t)),
297        AuthPayload::None => err(ErrorKind::Empty, value.len()),
298        AuthPayload::Params(p) => {
299            let at = (p.text.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
300            err(ErrorKind::InvalidByte, at)
301        }
302    }
303}
304
305/// The base64 text of a `Basic` `Authorization` value (RFC 7617): `Some`
306/// when the scheme is `Basic`, `None` for another scheme. The text is
307/// checked (the base64 alphabet, padding only at the end, a length that is a
308/// multiple of four) and not decoded.
309///
310/// # Errors
311///
312/// [`ErrorKind::InvalidBase64`]; [`ErrorKind::Empty`] for `Basic` alone;
313/// the scheme errors.
314pub fn basic(value: &[u8]) -> Result<Option<&[u8]>> {
315    let c = parse_credentials(value, &Limits::DEFAULT)?;
316    if !c.scheme_is("basic") {
317        return Ok(None);
318    }
319    let t = match c.payload {
320        AuthPayload::Token68(t) => t,
321        AuthPayload::None => return err(ErrorKind::Empty, value.len()),
322        AuthPayload::Params(p) => {
323            let at = (p.text.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
324            return err(ErrorKind::InvalidBase64, at);
325        }
326    };
327    let at = (t.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
328    let body = t.iter().take_while(|&&b| class::is_base64_char(b)).count();
329    let pad = t.len() - body;
330    if body < t.len() && t.get(body) != Some(&b'=') || pad > 2 || t.len() % 4 != 0 {
331        return err(ErrorKind::InvalidBase64, at + body);
332    }
333    Ok(Some(t))
334}
335
336/// One challenge.
337#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
338pub struct Challenge<'a> {
339    scheme: &'a [u8],
340    payload: AuthPayload<'a>,
341}
342
343impl<'a> Challenge<'a> {
344    /// The scheme, as sent.
345    #[must_use]
346    pub const fn scheme(&self) -> &'a [u8] {
347        self.scheme
348    }
349
350    /// Whether the scheme is `s` (ASCII case-insensitive).
351    #[must_use]
352    pub fn scheme_is(&self, s: &str) -> bool {
353        self.scheme.eq_ignore_ascii_case(s.as_bytes())
354    }
355
356    /// The payload.
357    #[must_use]
358    pub const fn payload(&self) -> AuthPayload<'a> {
359        self.payload
360    }
361
362    /// The parameter `name` (`realm`, `error`, `scope`, ...).
363    #[must_use]
364    pub fn param(&self, name: &str) -> Option<Word<'a>> {
365        match self.payload {
366            AuthPayload::Params(p) => p.get(name),
367            _ => None,
368        }
369    }
370}
371
372/// Validated challenges.
373#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
374pub struct Challenges<'a> {
375    list: list::List<'a>,
376    value: &'a [u8],
377    count: usize,
378}
379
380impl<'a> Challenges<'a> {
381    /// The challenges, in order.
382    #[must_use]
383    pub fn iter(&self) -> ChallengeIter<'a> {
384        ChallengeIter {
385            value: self.value,
386            elements: self.list.iter().peekable(),
387        }
388    }
389
390    /// The number of challenges.
391    #[must_use]
392    pub const fn len(&self) -> usize {
393        self.count
394    }
395
396    /// Whether there are no challenges.
397    #[must_use]
398    pub const fn is_empty(&self) -> bool {
399        self.count == 0
400    }
401}
402
403/// The challenges of a validated value.
404#[derive(Clone, Debug)]
405pub struct ChallengeIter<'a> {
406    value: &'a [u8],
407    elements: Peekable<list::Elements<'a>>,
408}
409
410/// The start of challenge element `e`: its scheme and what follows.
411fn challenge_start(e: &[u8]) -> (&[u8], &[u8]) {
412    let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
413    let scheme = e.get(..n).unwrap_or(&[]);
414    let rest = e.get(n..).unwrap_or(&[]);
415    let rest = rest
416        .get(rest.iter().take_while(|&&b| b == b' ').count()..)
417        .unwrap_or(&[]);
418    (scheme, rest)
419}
420
421impl<'a> Iterator for ChallengeIter<'a> {
422    type Item = Challenge<'a>;
423
424    fn next(&mut self) -> Option<Challenge<'a>> {
425        let e = self.elements.next()?;
426        let (scheme, rest) = challenge_start(e);
427        if rest.is_empty() {
428            return Some(Challenge {
429                scheme,
430                payload: AuthPayload::None,
431            });
432        }
433        if is_token68(rest) {
434            return Some(Challenge {
435                scheme,
436                payload: AuthPayload::Token68(rest),
437            });
438        }
439        // The first parameter is in this element; the rest follow it as
440        // separate elements until the next challenge starts.
441        let start = (rest.as_ptr() as usize).saturating_sub(self.value.as_ptr() as usize);
442        let mut end = start + rest.len();
443        let mut count = 1usize;
444        while let Some(next) = self.elements.peek() {
445            if !is_param_start(next) {
446                break;
447            }
448            end =
449                (next.as_ptr() as usize).saturating_sub(self.value.as_ptr() as usize) + next.len();
450            count += 1;
451            let _skipped = self.elements.next();
452        }
453        let text = self.value.get(start..end).unwrap_or(&[]);
454        Some(Challenge {
455            scheme,
456            payload: AuthPayload::Params(AuthParams { text, count }),
457        })
458    }
459}
460
461/// Parses `WWW-Authenticate` or `Proxy-Authenticate`: `#challenge`.
462///
463/// A challenge's own parameters are comma-separated too, so where one
464/// challenge ends is decided element by element: a token followed by `=` is
465/// a parameter of the current challenge; a token alone, or a token and
466/// `1*SP` before a `token68` or a parameter, starts a new one. A parameter
467/// name may not repeat within one challenge (RFC 9110 section 11.2).
468///
469/// # Errors
470///
471/// [`ErrorKind::DuplicateParameter`]; [`ErrorKind::NotToken`];
472/// [`ErrorKind::ExpectedSeparator`] for a parameter before any challenge;
473/// [`ErrorKind::TrailingData`]; the list and quoted-string errors.
474///
475/// # Examples
476///
477/// ```
478/// use parse_net::{auth, Limits};
479///
480/// let c = auth::parse_challenges(
481///     b"Newauth realm=\"apps\", type=1, title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"",
482///     &Limits::DEFAULT)?;
483/// let all: Vec<_> = c.iter().collect();
484/// assert_eq!(all.len(), 2);
485/// assert!(all[0].param("type").is_some_and(|v| v.eq_bytes(b"1")));
486/// assert!(all[1].scheme_is("basic"));
487/// # Ok::<(), parse_net::ParseError>(())
488/// ```
489pub fn parse_challenges<'a>(value: &'a [u8], limits: &Limits) -> Result<Challenges<'a>> {
490    let l = list::parse(value, limits)?;
491    let mut count = 0usize;
492    let mut elements = l.iter().peekable();
493    while let Some(e) = elements.next() {
494        let at = (e.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
495        if is_param_start(e) {
496            return err(ErrorKind::ExpectedSeparator, at);
497        }
498        let n = e.iter().take_while(|&&b| class::is_tchar(b)).count();
499        if n == 0 {
500            return Err(refused(e, 0, ErrorKind::NotToken).shifted(at));
501        }
502        let (_, rest) = challenge_start(e);
503        if e.get(n).is_some_and(|&b| b != b' ') {
504            return Err(refused(e, n, ErrorKind::NotToken).shifted(at));
505        }
506        count += 1;
507        if rest.is_empty() || is_token68(rest) {
508            continue;
509        }
510        if !is_param_start(rest) {
511            let off = e.len() - rest.len();
512            return Err(refused(e, off, ErrorKind::TrailingData).shifted(at));
513        }
514        // This challenge's parameters: the rest of this element and every
515        // following parameter element.
516        let start = at + (e.len() - rest.len());
517        let mut end = at + e.len();
518        while let Some(next) = elements.peek() {
519            if !is_param_start(next) {
520                break;
521            }
522            end = (next.as_ptr() as usize).saturating_sub(value.as_ptr() as usize) + next.len();
523            let _skipped = elements.next();
524        }
525        let _params = check_params(value.get(start..end).unwrap_or(&[]), start, limits)?;
526    }
527    Ok(Challenges {
528        list: l,
529        value,
530        count,
531    })
532}
533
534fn put_challenge(
535    scheme: &str,
536    params: &[(&str, &str)],
537    out: &mut impl Sink,
538) -> core::result::Result<(), WriteError> {
539    out.put(scheme.as_bytes())?;
540    for (i, (name, value)) in params.iter().enumerate() {
541        out.put(if i == 0 { b" " } else { b", " })?;
542        out.put(name.as_bytes())?;
543        out.put(b"=")?;
544        if name.eq_ignore_ascii_case("realm") {
545            write_quoted(value.as_bytes(), out)?;
546        } else {
547            write_word(value.as_bytes(), out)?;
548        }
549    }
550    Ok(())
551}
552
553/// Writes a challenge with parameters: `Bearer realm="api", error="invalid_token"`.
554/// `realm` is always quoted (RFC 9110 section 11.5); other values are tokens
555/// when they can be.
556///
557/// # Errors
558///
559/// [`WriteError::NotToken`] for the scheme or a name;
560/// [`WriteError::DuplicateName`] for a name given twice (RFC 9110 section
561/// 11.2 allows each once); [`WriteError::Forbidden`] for a control byte in a
562/// value; [`WriteError::Full`] (nothing written when the sink reports its
563/// room).
564///
565/// # Examples
566///
567/// ```
568/// use parse_net::{auth, SliceSink};
569///
570/// let mut buf = [0u8; 64];
571/// let mut out = SliceSink::new(&mut buf);
572/// auth::write_challenge("Bearer", &[("realm", "api"), ("error", "invalid_token")], &mut out).expect("room");
573/// assert_eq!(out.written(), b"Bearer realm=\"api\", error=invalid_token");
574/// ```
575pub fn write_challenge(
576    scheme: &str,
577    params: &[(&str, &str)],
578    out: &mut impl Sink,
579) -> core::result::Result<(), WriteError> {
580    if !is_token(scheme.as_bytes()) || params.iter().any(|(n, _)| !is_token(n.as_bytes())) {
581        return Err(WriteError::NotToken);
582    }
583    unique_names(params)?;
584    let mut count = CountSink::new();
585    put_challenge(scheme, params, &mut count)?;
586    reserve(out, count.count())?;
587    put_challenge(scheme, params, out)
588}
589
590/// Writes a challenge with a `token68`: `Negotiate <token68>`.
591///
592/// # Errors
593///
594/// [`WriteError::NotToken`] for the scheme; [`WriteError::Forbidden`] when
595/// `token68` is not a `token68`; [`WriteError::Full`].
596///
597/// # Examples
598///
599/// ```
600/// use parse_net::{auth, SliceSink};
601///
602/// let mut buf = [0u8; 32];
603/// let mut out = SliceSink::new(&mut buf);
604/// auth::write_challenge_token68("Negotiate", b"YIIB==", &mut out).expect("room");
605/// assert_eq!(out.written(), b"Negotiate YIIB==");
606/// ```
607pub fn write_challenge_token68(
608    scheme: &str,
609    token68: &[u8],
610    out: &mut impl Sink,
611) -> core::result::Result<(), WriteError> {
612    if !is_token(scheme.as_bytes()) {
613        return Err(WriteError::NotToken);
614    }
615    if !is_token68(token68) {
616        let at = token68
617            .iter()
618            .position(|&b| !(class::is_token68_char(b) || b == b'='))
619            .unwrap_or(0);
620        return Err(WriteError::forbidden(at));
621    }
622    reserve(out, scheme.len() + 1 + token68.len())?;
623    out.put(scheme.as_bytes())?;
624    out.put(b" ")?;
625    out.put(token68)?;
626    Ok(())
627}
628
629#[cfg(test)]
630mod tests {
631    use std::{format, vec::Vec};
632
633    use super::*;
634
635    #[test]
636    fn test_bearer_cases() {
637        assert_eq!(bearer(b"Bearer abc").unwrap(), Some(&b"abc"[..]));
638        assert_eq!(bearer(b"bearer abc").unwrap(), Some(&b"abc"[..]));
639        assert_eq!(bearer(b"Bearer  abc").unwrap(), Some(&b"abc"[..]));
640        assert_eq!(bearer(b"Bearer abc==").unwrap(), Some(&b"abc=="[..]));
641        assert_eq!(bearer(b"Bearer").unwrap_err().kind(), ErrorKind::Empty);
642        assert_eq!(
643            bearer(b"Bearer abc def").unwrap_err().kind(),
644            ErrorKind::TrailingData
645        );
646        assert_eq!(
647            bearer(b"Bearer a=b").unwrap_err().kind(),
648            ErrorKind::InvalidByte
649        );
650        assert_eq!(
651            bearer(b"Bearer\tabc").unwrap_err().kind(),
652            ErrorKind::NotToken
653        );
654        assert_eq!(bearer(b"").unwrap_err().kind(), ErrorKind::NotToken);
655        assert_eq!(bearer(b"Digest a=b").unwrap(), None);
656    }
657
658    #[test]
659    fn test_basic_cases() {
660        assert_eq!(
661            basic(b"Basic dXNlcjpwYXNz").unwrap(),
662            Some(&b"dXNlcjpwYXNz"[..])
663        );
664        assert_eq!(basic(b"Basic YQ==").unwrap(), Some(&b"YQ=="[..]));
665        assert_eq!(basic(b"Basic").unwrap_err().kind(), ErrorKind::Empty);
666        assert_eq!(
667            basic(b"Basic YQ").unwrap_err().kind(),
668            ErrorKind::InvalidBase64
669        );
670        assert_eq!(
671            basic(b"Basic a-b_").unwrap_err().kind(),
672            ErrorKind::InvalidBase64
673        );
674        assert_eq!(
675            basic(b"Basic YQ===").unwrap_err().kind(),
676            ErrorKind::InvalidBase64
677        );
678        assert_eq!(basic(b"Bearer x").unwrap(), None);
679    }
680
681    #[test]
682    fn test_credentials_params_and_duplicates() {
683        let l = Limits::DEFAULT;
684        let c = parse_credentials(b"Digest username = \"a\" , realm=r, ", &l).unwrap();
685        let AuthPayload::Params(p) = c.payload() else {
686            panic!("params")
687        };
688        assert_eq!(p.len(), 2);
689        assert!(p.get("USERNAME").unwrap().eq_bytes(b"a"));
690        assert_eq!(
691            parse_credentials(b"Digest a=b, A=c", &l)
692                .unwrap_err()
693                .kind(),
694            ErrorKind::DuplicateParameter
695        );
696        assert_eq!(
697            parse_credentials(b"Negotiate token68===", &l)
698                .unwrap()
699                .payload(),
700            AuthPayload::Token68(b"token68===")
701        );
702        assert_eq!(
703            parse_credentials(b"Digest a", &l).unwrap().payload(),
704            AuthPayload::Token68(b"a")
705        );
706        assert_eq!(
707            parse_credentials(b"Digest a b", &l).unwrap_err().kind(),
708            ErrorKind::TrailingData
709        );
710        // `a=` is a token68 with padding; `a=, b` is a parameter with no value.
711        assert_eq!(
712            parse_credentials(b"Digest a=", &l).unwrap().payload(),
713            AuthPayload::Token68(b"a=")
714        );
715        assert_eq!(
716            parse_credentials(b"Digest a=, b=1", &l).unwrap_err().kind(),
717            ErrorKind::MissingValue
718        );
719        assert_eq!(
720            parse_credentials(b"Digest a=\"x", &l).unwrap_err().kind(),
721            ErrorKind::UnterminatedQuote
722        );
723    }
724
725    #[test]
726    fn test_credentials_debug_redacted() {
727        let c = parse_credentials(b"Bearer s3cr3t-token", &Limits::DEFAULT).unwrap();
728        let d = format!("{c:?} {:?}", c.payload());
729        assert!(!d.contains("s3cr3t"));
730        let p = parse_credentials(b"Digest response=\"s3cr3t\"", &Limits::DEFAULT).unwrap();
731        let d = format!("{:?}", p.payload());
732        assert_eq!(d, "AuthParams([\"response\"])");
733    }
734
735    #[test]
736    fn test_challenges_rfc_examples() {
737        let l = Limits::DEFAULT;
738        let names = |v: &[u8]| -> Vec<Vec<u8>> {
739            parse_challenges(v, &l)
740                .unwrap()
741                .iter()
742                .map(|c| c.scheme().to_vec())
743                .collect()
744        };
745        assert_eq!(
746            names(b"Basic realm=\"a\", Bearer"),
747            [b"Basic".to_vec(), b"Bearer".to_vec()]
748        );
749        let c = parse_challenges(b"Bearer realm=\"a\", error=\"invalid_token\"", &l).unwrap();
750        let one: Vec<_> = c.iter().collect();
751        assert_eq!(one.len(), 1);
752        assert!(one[0].param("error").unwrap().eq_bytes(b"invalid_token"));
753        assert_eq!(
754            names(b"Negotiate abc==, Basic realm=x"),
755            [b"Negotiate".to_vec(), b"Basic".to_vec()]
756        );
757        assert_eq!(
758            names(b"A, , B, ,C"),
759            [b"A".to_vec(), b"B".to_vec(), b"C".to_vec()]
760        );
761        let t68 = parse_challenges(b"Negotiate abc==", &l).unwrap();
762        assert_eq!(
763            t68.iter().next().unwrap().payload(),
764            AuthPayload::Token68(b"abc==")
765        );
766    }
767
768    #[test]
769    fn test_challenge_refusals() {
770        let l = Limits::DEFAULT;
771        let k = |v: &[u8]| parse_challenges(v, &l).unwrap_err().kind();
772        assert_eq!(k(b"Basic realm=a, realm=b"), ErrorKind::DuplicateParameter);
773        assert_eq!(k(b"realm=a"), ErrorKind::ExpectedSeparator);
774        assert_eq!(k(b"Basic\trealm=a"), ErrorKind::NotToken);
775        assert_eq!(k(b"Basic abc def"), ErrorKind::TrailingData);
776        assert!(parse_challenges(b"Basic realm=a, Digest realm=b", &l).is_ok());
777    }
778
779    #[test]
780    fn test_challenge_writers_round_trip() {
781        let mut buf = [0u8; 128];
782        let mut out = crate::SliceSink::new(&mut buf);
783        write_challenge(
784            "Bearer",
785            &[("realm", "api"), ("scope", "read write")],
786            &mut out,
787        )
788        .unwrap();
789        let c = parse_challenges(out.written(), &Limits::DEFAULT).unwrap();
790        let first = c.iter().next().unwrap();
791        assert!(first.param("realm").unwrap().eq_bytes(b"api"));
792        assert!(first.param("scope").unwrap().eq_bytes(b"read write"));
793        assert_eq!(
794            write_challenge("Bad scheme", &[], &mut out),
795            Err(WriteError::NotToken)
796        );
797        assert_eq!(
798            write_challenge("Basic", &[("realm", "a"), ("Realm", "b")], &mut out),
799            Err(WriteError::DuplicateName)
800        );
801        assert_eq!(
802            write_challenge("Basic", &[("realm", "a\r\n")], &mut out),
803            Err(WriteError::Forbidden { at: 1 })
804        );
805        assert_eq!(
806            write_challenge_token68("Negotiate", b"a b", &mut out),
807            Err(WriteError::Forbidden { at: 1 })
808        );
809    }
810}