entropy-auth 2026.7.31

Authentication and authorization for Entropy Softworks server and API projects
//! Bearer token extraction from HTTP Authorization headers.
//!
//! Extracts and validates the token portion of `Authorization: Bearer <token>`
//! headers per RFC 6750. The token must consist of printable ASCII characters
//! and is bounded to a maximum length to prevent abuse.

use core::fmt;

use crate::util::log::{debug, warn};

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/// Exact prefix required for the Authorization header value.
const BEARER_PREFIX: &str = "Bearer ";

/// Maximum allowed length of the full header value (prefix + token).
/// 4096 bytes is generous for any realistic bearer token (JWTs, opaque
/// tokens, API keys) while still bounding memory allocation.
const MAX_HEADER_LEN: usize = 4096;

// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------

/// The kind of error encountered during bearer token extraction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BearerErrorKind {
    /// Header does not start with `"Bearer "` (case-sensitive).
    MissingPrefix,
    /// Token portion after `"Bearer "` is empty.
    EmptyToken,
    /// Token contains non-printable or non-ASCII characters.
    InvalidCharacters,
    /// Header value exceeds the maximum allowed length.
    TooLong,
}

/// Error returned when bearer token extraction fails.
///
/// Error messages never reveal the token value — only the nature of the
/// failure is described.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BearerError {
    kind: BearerErrorKind,
}

impl BearerError {
    /// Returns `true` if the header was missing the `"Bearer "` prefix.
    #[must_use]
    pub fn is_missing_prefix(&self) -> bool {
        self.kind == BearerErrorKind::MissingPrefix
    }

    /// Returns `true` if the token portion was empty.
    #[must_use]
    pub fn is_empty_token(&self) -> bool {
        self.kind == BearerErrorKind::EmptyToken
    }

    /// Returns `true` if the token contained invalid characters.
    #[must_use]
    pub fn is_invalid_characters(&self) -> bool {
        self.kind == BearerErrorKind::InvalidCharacters
    }

    /// Returns `true` if the header exceeded the maximum length.
    #[must_use]
    pub fn is_too_long(&self) -> bool {
        self.kind == BearerErrorKind::TooLong
    }
}

impl fmt::Display for BearerError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.kind {
            BearerErrorKind::MissingPrefix => {
                write!(f, "bearer: missing \"Bearer \" prefix")
            }
            BearerErrorKind::EmptyToken => {
                write!(f, "bearer: token is empty")
            }
            BearerErrorKind::InvalidCharacters => {
                write!(
                    f,
                    "bearer: token contains non-printable or non-ASCII characters"
                )
            }
            BearerErrorKind::TooLong => {
                write!(
                    f,
                    "bearer: header exceeds maximum length of {MAX_HEADER_LEN} bytes"
                )
            }
        }
    }
}

impl std::error::Error for BearerError {}

// ---------------------------------------------------------------------------
// Extraction
// ---------------------------------------------------------------------------

/// Extracts the token from an HTTP Authorization header in `"Bearer <token>"`
/// format.
///
/// # Validation
///
/// - The header must start with exactly `"Bearer "` (case-sensitive, with a
///   trailing space).
/// - The token portion must be non-empty.
/// - Every character in the token must be printable ASCII (0x20..=0x7E).
/// - The total header length must not exceed 4096 bytes.
///
/// # Errors
///
/// Returns [`BearerError`] if any validation check fails. The error message
/// describes the failure without revealing the token value.
///
/// # Examples
///
/// ```
/// use entropy_auth::api::extract_bearer_token;
///
/// let token = extract_bearer_token("Bearer my-secret-token").unwrap();
/// assert_eq!(token, "my-secret-token");
/// ```
pub fn extract_bearer_token(header: &str) -> Result<&str, BearerError> {
    // Length check first to prevent processing oversized inputs.
    if header.len() > MAX_HEADER_LEN {
        warn!("bearer: extraction failed (too long)");
        return Err(BearerError {
            kind: BearerErrorKind::TooLong,
        });
    }

    // The prefix is case-sensitive per RFC 6750 §2.1: the scheme name
    // "Bearer" is defined with a capital B and lowercase remainder.
    let token = header.strip_prefix(BEARER_PREFIX).ok_or_else(|| {
        warn!("bearer: extraction failed (missing prefix)");
        BearerError {
            kind: BearerErrorKind::MissingPrefix,
        }
    })?;

    if token.is_empty() {
        warn!("bearer: extraction failed (empty token)");
        return Err(BearerError {
            kind: BearerErrorKind::EmptyToken,
        });
    }

    // SECURITY: Reject tokens containing control characters, DEL (0x7F),
    // or non-ASCII bytes. This prevents injection of invisible characters
    // into downstream systems that consume the token string.
    if !token.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
        warn!("bearer: extraction failed (invalid characters)");
        return Err(BearerError {
            kind: BearerErrorKind::InvalidCharacters,
        });
    }

    // SECURITY: Log only the token length — never the token value.
    debug!(len = token.len(), "bearer: token extracted");

    Ok(token)
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    // --- Valid Extraction ---

    #[test]
    fn valid_bearer_token() {
        let token = extract_bearer_token("Bearer abc123").unwrap();
        assert_eq!(token, "abc123");
    }

    #[test]
    fn valid_bearer_token_with_special_chars() {
        // Printable ASCII includes symbols, digits, letters.
        let token = extract_bearer_token("Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig").unwrap();
        assert_eq!(token, "eyJhbGciOiJIUzI1NiJ9.payload.sig");
    }

    #[test]
    fn valid_bearer_token_with_spaces_in_token() {
        // Spaces (0x20) are printable ASCII.
        let token = extract_bearer_token("Bearer token with spaces").unwrap();
        assert_eq!(token, "token with spaces");
    }

    #[test]
    fn valid_bearer_token_single_char() {
        let token = extract_bearer_token("Bearer x").unwrap();
        assert_eq!(token, "x");
    }

    // --- Missing Prefix ---

    #[test]
    fn missing_prefix_entirely() {
        let err = extract_bearer_token("abc123").unwrap_err();
        assert!(err.is_missing_prefix());
    }

    #[test]
    fn missing_prefix_no_space() {
        // "Bearer" without trailing space is not a valid prefix.
        let err = extract_bearer_token("Bearerabc123").unwrap_err();
        assert!(err.is_missing_prefix());
    }

    #[test]
    fn lowercase_bearer_rejected() {
        // RFC 6750 requires case-sensitive "Bearer ".
        let err = extract_bearer_token("bearer abc123").unwrap_err();
        assert!(err.is_missing_prefix());
    }

    #[test]
    fn uppercase_bearer_rejected() {
        let err = extract_bearer_token("BEARER abc123").unwrap_err();
        assert!(err.is_missing_prefix());
    }

    #[test]
    fn empty_header() {
        let err = extract_bearer_token("").unwrap_err();
        assert!(err.is_missing_prefix());
    }

    #[test]
    fn just_bearer_prefix_no_token() {
        // "Bearer " with nothing after it.
        let err = extract_bearer_token("Bearer ").unwrap_err();
        assert!(err.is_empty_token());
    }

    // --- Empty Token ---

    #[test]
    fn empty_token_after_prefix() {
        let err = extract_bearer_token("Bearer ").unwrap_err();
        assert!(err.is_empty_token());
    }

    // --- Invalid Characters ---

    #[test]
    fn token_with_null_byte() {
        let err = extract_bearer_token("Bearer abc\x00def").unwrap_err();
        assert!(err.is_invalid_characters());
    }

    #[test]
    fn token_with_newline() {
        let err = extract_bearer_token("Bearer abc\ndef").unwrap_err();
        assert!(err.is_invalid_characters());
    }

    #[test]
    fn token_with_tab() {
        let err = extract_bearer_token("Bearer abc\tdef").unwrap_err();
        assert!(err.is_invalid_characters());
    }

    #[test]
    fn token_with_del() {
        let err = extract_bearer_token("Bearer abc\x7Fdef").unwrap_err();
        assert!(err.is_invalid_characters());
    }

    #[test]
    fn token_with_non_ascii() {
        let err = extract_bearer_token("Bearer caf\u{00e9}").unwrap_err();
        assert!(err.is_invalid_characters());
    }

    // --- Too Long ---

    #[test]
    fn token_at_max_length_accepted() {
        // "Bearer " is 7 bytes, so the token can be up to MAX_HEADER_LEN - 7.
        let token_len = MAX_HEADER_LEN - BEARER_PREFIX.len();
        let header = format!("Bearer {}", "a".repeat(token_len));
        assert_eq!(header.len(), MAX_HEADER_LEN);
        let result = extract_bearer_token(&header);
        assert!(result.is_ok());
        assert_eq!(result.unwrap().len(), token_len);
    }

    #[test]
    fn token_exceeding_max_length_rejected() {
        let header = format!("Bearer {}", "a".repeat(MAX_HEADER_LEN));
        let err = extract_bearer_token(&header).unwrap_err();
        assert!(err.is_too_long());
    }

    // --- Display ---

    #[test]
    fn error_display_messages() {
        let cases = [
            (
                BearerError {
                    kind: BearerErrorKind::MissingPrefix,
                },
                "bearer: missing \"Bearer \" prefix",
            ),
            (
                BearerError {
                    kind: BearerErrorKind::EmptyToken,
                },
                "bearer: token is empty",
            ),
            (
                BearerError {
                    kind: BearerErrorKind::InvalidCharacters,
                },
                "bearer: token contains non-printable or non-ASCII characters",
            ),
            (
                BearerError {
                    kind: BearerErrorKind::TooLong,
                },
                &format!("bearer: header exceeds maximum length of {MAX_HEADER_LEN} bytes"),
            ),
        ];
        for (err, expected) in &cases {
            assert_eq!(err.to_string(), *expected);
        }
    }

    #[test]
    fn error_implements_std_error() {
        let err: Box<dyn std::error::Error> = Box::new(BearerError {
            kind: BearerErrorKind::MissingPrefix,
        });
        let _ = err.to_string();
    }
}