fraiseql-auth 2.2.0

Authentication, authorization, and session management for FraiseQL
Documentation
//! Security tests for authentication and cryptographic operations.
//! Validates that security-critical operations meet production standards.
#[cfg(test)]
use std::collections::{HashMap, HashSet};

/// Test that CSRF tokens are cryptographically unique and unpredictable.
/// Generates multiple tokens and verifies no collisions and high entropy.
#[test]
fn test_csrf_token_uniqueness_and_entropy() {
    use crate::handlers::generate_secure_state;

    let mut tokens = HashSet::new();
    let iterations = 100;

    for _ in 0..iterations {
        let token = generate_secure_state();

        // Verify minimum length for cryptographic security
        assert!(
            token.len() >= 64,
            "CSRF token too short: {} (should be >= 64 hex chars = 256 bits)",
            token.len()
        );

        // Verify hex encoding (only 0-9a-f)
        assert!(
            token.chars().all(|c| c.is_ascii_hexdigit()),
            "Token contains non-hex characters: {}",
            token
        );

        // Track for collision detection
        tokens.insert(token);
    }

    // Verify no collisions
    assert_eq!(
        tokens.len(),
        iterations,
        "CSRF token collisions detected! Only {} unique out of {}",
        tokens.len(),
        iterations
    );
}

/// Test that CSRF state generation produces cryptographically secure values.
/// OsRng should be used for cryptographic randomness, not thread_rng.
#[test]
fn test_csrf_state_is_cryptographically_random() {
    use crate::handlers::generate_secure_state;

    // Generate multiple states
    let states: Vec<String> = (0..50).map(|_| generate_secure_state()).collect();

    // Verify each is unique (no collisions)
    let unique_count = states.iter().collect::<HashSet<_>>().len();
    assert_eq!(
        unique_count, 50,
        "CSRF state generator produced duplicates! Only {} unique",
        unique_count
    );

    // Verify hex format
    for state in &states {
        assert!(hex::decode(state).is_ok(), "CSRF state is not valid hex: {}", state);
    }
}

/// Test that JWT expiration is properly enforced.
/// Expired tokens must be rejected, not silently accepted.
#[test]
fn test_jwt_expiration_enforcement() {
    use std::time::{SystemTime, UNIX_EPOCH};

    use crate::jwt::Claims;

    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("System time error")
        .as_secs();

    // Create expired token (exp = 1 second ago)
    let expired_token = Claims {
        iss:   "test_issuer".to_string(),
        sub:   "user123".to_string(),
        aud:   vec!["api".to_string()],
        exp:   now - 1,
        iat:   now - 3600,
        extra: HashMap::default(),
    };

    assert!(expired_token.is_expired(), "Expired token should be rejected");

    // Create future token (exp = 1 hour from now)
    let valid_token = Claims {
        iss:   "test_issuer".to_string(),
        sub:   "user123".to_string(),
        aud:   vec!["api".to_string()],
        exp:   now + 3600,
        iat:   now,
        extra: HashMap::default(),
    };

    assert!(!valid_token.is_expired(), "Valid token should not be rejected");
}

/// Test that JWT validator can be configured with audience validation.
#[test]
fn test_jwt_audience_validation_support() {
    use jsonwebtoken::Algorithm;

    use crate::jwt::JwtValidator;

    // Create validator without audiences (backward compat)
    let validator = JwtValidator::new("https://issuer.example.com", Algorithm::HS256)
        .expect("Valid issuer config");

    // Configure with audiences
    let _validator_with_aud = validator.with_audiences(&["api", "web"]).expect("Valid audiences");

    // This test validates that the API supports audience configuration
    // The actual enforcement happens in production when tokens are validated
}

/// Test that invalid issuer is rejected.
#[test]
fn test_jwt_invalid_issuer_rejection() {
    use jsonwebtoken::Algorithm;

    use crate::jwt::JwtValidator;

    // Empty issuer should fail
    let result = JwtValidator::new("", Algorithm::HS256);

    assert!(result.is_err(), "Empty issuer should be rejected");
}

/// Test that CSRF token format is consistent and URL-safe.
#[test]
fn test_csrf_token_url_safe_format() {
    use crate::handlers::generate_secure_state;

    let tokens: Vec<String> = (0..20).map(|_| generate_secure_state()).collect();

    for token in tokens {
        // Must be hex (URL-safe without encoding)
        assert!(
            token.chars().all(|c| c.is_ascii_hexdigit()),
            "Token should be hex-safe for URLs: {}",
            token
        );

        // Must be deterministic length (32 bytes = 64 hex chars)
        assert_eq!(token.len(), 64, "Token length should be consistent: {}", token.len());
    }
}

/// Test that state storage properly rejects expired states.
/// This is a property test that state expiry is enforced.
#[test]
fn test_state_expiry_property() {
    // This test documents the expected behavior:
    // - State generated now should be valid
    // - State that expired 1 second ago should be rejected

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("System time error")
        .as_secs();

    let future_expiry = now + 600; // 10 minutes
    let past_expiry = now - 1; // Already expired

    assert!(future_expiry > now, "Future expiry should be after current time");

    assert!(past_expiry < now, "Past expiry should be before current time");
}

/// Test that random state generation doesn't use weak RNG.
/// Verifies the implementation uses OsRng for cryptographic randomness.
#[test]
fn test_randomness_quality() {
    use crate::handlers::generate_secure_state;

    // Generate states with different byte patterns
    let states: Vec<String> = (0..10).map(|_| generate_secure_state()).collect();

    // Verify we have good distribution (no obvious patterns)
    for state in states {
        // Decode hex
        let bytes = hex::decode(&state).expect("Valid hex");

        // Count bit transitions (high entropy indicator)
        let mut transitions = 0;
        for i in 0..bytes.len() - 1 {
            if bytes[i] != bytes[i + 1] {
                transitions += 1;
            }
        }

        // With cryptographic randomness, expect ~50% transitions
        // Very conservative minimum: 20%
        let byte_count = bytes.len();
        let min_transitions = byte_count / 5;

        assert!(
            transitions > min_transitions,
            "Insufficient entropy in random bytes: {} transitions in {} bytes",
            transitions,
            byte_count
        );
    }
}