jwt_extractor.rs 178 lignes · 5692 octets
//! JWT token extraction from HTTP requests
//!
//! This module provides functionality to extract JWT tokens from HTTP requests,
//! supporting both cookie-based (web) and header-based (API) authentication.
//!
//! # Extraction Priority
//!
//! 1. **Authorization Header**: `Authorization: Bearer <token>` (for API clients)
//! 2. **Cookie**: `jwt_token` cookie (for web browsers)
//!
//! This priority allows API clients to override cookie-based authentication
//! by explicitly providing a token in the Authorization header.

use axum::extract::Request;
use axum_extra::extract::CookieJar;

use crate::error::AppError;

/// Extract JWT token from HTTP request
///
/// Attempts to extract a JWT token from the request in the following order:
/// 1. `Authorization: Bearer <token>` header (for API)
/// 2. `jwt_token` cookie (for web)
///
/// # Arguments
///
/// * `request` - The HTTP request
///
/// # Returns
///
/// `Ok(String)` with the JWT token if found, `AppError` otherwise
///
/// # Errors
///
/// Returns `AppError::Unauthorized` if no token is found in either location
///
/// # Examples
///
/// ```rust,ignore
/// use rustwarden_core::auth::jwt_extractor::extract_jwt_from_request;
///
/// // In a middleware or handler
/// let jwt_token = extract_jwt_from_request(&request)?;
/// ```
pub fn extract_jwt_from_request(request: &Request) -> Result<String, AppError> {
    // Priority 1: Check Authorization header (for API)
    if let Some(auth_header) = request.headers().get("authorization") {
        if let Ok(auth_str) = auth_header.to_str() {
            if let Some(token) = extract_bearer_token(auth_str) {
                tracing::debug!("JWT extracted from Authorization header");
                return Ok(token);
            }
        }
    }

    // Priority 2: Check cookie (for web)
    let cookie_jar = CookieJar::from_headers(request.headers());
    if let Some(cookie) = cookie_jar.get("jwt_token") {
        let token = cookie.value().to_string();
        if !token.is_empty() {
            tracing::debug!("JWT extracted from cookie");
            return Ok(token);
        }
    }

    // No token found
    tracing::debug!("No JWT token found in request");
    Err(AppError::InvalidToken)
}

/// Extract JWT token from request parts (headers only, no body needed)
///
/// This function extracts a JWT token from request parts (headers/cookies).
/// Useful when you only have access to request parts, not the full request.
///
/// # Arguments
///
/// * `parts` - Request parts (contains headers, URI, etc.)
///
/// # Returns
///
/// `Ok(String)` with the JWT token if found, `AppError` otherwise
pub fn extract_jwt_from_request_parts(
    parts: &axum::http::request::Parts,
) -> Result<String, AppError> {
    // Priority 1: Check Authorization header (for API)
    if let Some(auth_header) = parts.headers.get("authorization") {
        if let Ok(auth_str) = auth_header.to_str() {
            if let Some(token) = extract_bearer_token(auth_str) {
                tracing::debug!("JWT extracted from Authorization header");
                return Ok(token);
            }
        }
    }

    // Priority 2: Check cookie (for web)
    let cookie_jar = axum_extra::extract::CookieJar::from_headers(&parts.headers);
    if let Some(cookie) = cookie_jar.get("jwt_token") {
        let token = cookie.value().to_string();
        if !token.is_empty() {
            tracing::debug!("JWT extracted from cookie");
            return Ok(token);
        }
    }

    // No token found
    tracing::debug!("No JWT token found in request parts");
    Err(AppError::InvalidToken)
}

/// Extract Bearer token from Authorization header value
///
/// Parses strings like `"Bearer <token>"` and returns the token part.
///
/// # Arguments
///
/// * `auth_header` - The Authorization header value (e.g., "Bearer eyJhbGci...")
///
/// # Returns
///
/// `Some(String)` with the token if valid Bearer format, `None` otherwise
///
/// # Examples
///
/// ```
/// use rustwarden_core::auth::jwt_extractor::extract_bearer_token;
///
/// let token = extract_bearer_token("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...");
/// assert_eq!(token, Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...".to_string()));
/// ```
pub fn extract_bearer_token(auth_header: &str) -> Option<String> {
    // Check if it starts with "Bearer "
    if auth_header.len() > 7 && auth_header[..7].eq_ignore_ascii_case("bearer ") {
        let token = auth_header[7..].trim();
        if !token.is_empty() {
            return Some(token.to_string());
        }
    }
    None
}

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

    #[test]
    fn test_extract_bearer_token() {
        // Valid Bearer token
        assert_eq!(
            extract_bearer_token("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
            Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9".to_string())
        );

        // Bearer with extra spaces
        assert_eq!(
            extract_bearer_token("Bearer  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9  "),
            Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9".to_string())
        );

        // Case insensitive
        assert_eq!(
            extract_bearer_token("bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
            Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9".to_string())
        );

        // Invalid format
        assert_eq!(
            extract_bearer_token("Token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
            None
        );
        assert_eq!(
            extract_bearer_token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
            None
        );
        assert_eq!(extract_bearer_token("Bearer"), None);
        assert_eq!(extract_bearer_token(""), None);
    }
}