rust-webx-host 0.3.0

rust-webx HTTP layer: Host builder, middleware pipeline, Trie-based router, hyper integration
Documentation
mod test_utils;

use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use rust_webx_core::auth::IAuthenticationHandler;
use rust_webx_host::auth_jwt::JwtAuth;
use serde::{Deserialize, Serialize};

fn now_plus_seconds(secs: u64) -> usize {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs() + secs)
        .unwrap() as usize
}

fn now_minus_seconds(secs: u64) -> usize {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().saturating_sub(secs))
        .unwrap() as usize
}

#[derive(Debug, Serialize, Deserialize)]
struct TestClaims {
    sub: String,
    #[serde(default)]
    roles: Vec<String>,
    #[serde(default)]
    permissions: Vec<String>,
    exp: usize,
}

fn create_test_token(secret: &[u8], sub: &str, roles: &[&str], permissions: &[&str]) -> String {
    let claims = TestClaims {
        sub: sub.to_string(),
        roles: roles.iter().map(|s| s.to_string()).collect(),
        permissions: permissions.iter().map(|s| s.to_string()).collect(),
        exp: now_plus_seconds(3600),
    };
    jsonwebtoken::encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(secret),
    )
    .unwrap()
}

fn create_expired_token(secret: &[u8]) -> String {
    let claims = TestClaims {
        sub: "expired-user".to_string(),
        roles: vec![],
        permissions: vec![],
        exp: now_minus_seconds(3600),
    };
    jsonwebtoken::encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(secret),
    )
    .unwrap()
}

#[tokio::test]
async fn auth_valid_token_returns_claims() {
    let secret = b"test-secret-key-for-auth-testing";
    let token = create_test_token(secret, "user-123", &["user"], &["read"]);
    let auth = JwtAuth::new(DecodingKey::from_secret(secret), Validation::default());

    let mut ctx = test_utils::TestHttpContext::new("GET", "/protected")
        .with_header("authorization", &format!("Bearer {}", token));

    let result = auth.authenticate(&mut ctx).await;
    assert!(result.is_ok());
    let claims = result.unwrap();
    assert!(claims.is_some());
    let claims = claims.unwrap();
    assert_eq!(claims.subject(), "user-123");
    assert!(claims.roles().contains(&"user".to_string()));
    assert!(claims.permissions().contains(&"read".to_string()));
}

#[tokio::test]
async fn auth_no_authorization_header_returns_none() {
    let secret = b"test-secret-key";
    let auth = JwtAuth::new(DecodingKey::from_secret(secret), Validation::default());

    let mut ctx = test_utils::TestHttpContext::new("GET", "/public");
    let result = auth.authenticate(&mut ctx).await.unwrap();
    assert!(result.is_none(), "No auth header should return None");
}

#[tokio::test]
async fn auth_empty_token_returns_none() {
    let secret = b"test-secret-key";
    let auth = JwtAuth::new(DecodingKey::from_secret(secret), Validation::default());

    let mut ctx =
        test_utils::TestHttpContext::new("GET", "/").with_header("authorization", "Bearer ");

    let result = auth.authenticate(&mut ctx).await.unwrap();
    assert!(result.is_none(), "Empty Bearer token should return None");
}

#[tokio::test]
async fn auth_invalid_token_returns_err() {
    let secret = b"test-secret-key";
    let auth = JwtAuth::new(DecodingKey::from_secret(secret), Validation::default());

    let mut ctx = test_utils::TestHttpContext::new("GET", "/")
        .with_header("authorization", "Bearer invalid-token-data");

    let result = auth.authenticate(&mut ctx).await;
    assert!(
        result.is_err(),
        "Invalid token should return Err"
    );
}

#[tokio::test]
async fn auth_wrong_secret_key_returns_err() {
    let secret = b"real-secret";
    let wrong_secret = b"wrong-secret";
    let token = create_test_token(secret, "user-1", &[], &[]);
    let auth = JwtAuth::new(
        DecodingKey::from_secret(wrong_secret),
        Validation::default(),
    );

    let mut ctx = test_utils::TestHttpContext::new("GET", "/")
        .with_header("authorization", &format!("Bearer {}", token));

    let result = auth.authenticate(&mut ctx).await;
    assert!(
        result.is_err(),
        "Token signed with different key should return Err"
    );
}

#[tokio::test]
async fn auth_expired_token_returns_err() {
    let secret = b"test-secret-key-expiry";
    let token = create_expired_token(secret);
    let auth = JwtAuth::new(DecodingKey::from_secret(secret), Validation::default());

    let mut ctx = test_utils::TestHttpContext::new("GET", "/")
        .with_header("authorization", &format!("Bearer {}", token));

    let result = auth.authenticate(&mut ctx).await;
    assert!(
        result.is_err(),
        "Expired token should return Err"
    );
}

#[tokio::test]
async fn auth_bearer_prefix_case_insensitive() {
    let secret = b"test-secret-key";
    let token = create_test_token(secret, "user-1", &[], &[]);
    let auth = JwtAuth::new(DecodingKey::from_secret(secret), Validation::default());

    let mut ctx = test_utils::TestHttpContext::new("GET", "/")
        .with_header("authorization", &format!("bearer {}", token));

    let result = auth.authenticate(&mut ctx).await.unwrap();
    assert!(
        result.is_some(),
        "Lowercase 'bearer' should be accepted (RFC 6750)"
    );
}

#[tokio::test]
async fn auth_token_with_extra_whitespace_handled() {
    let secret = b"test-secret-key";
    let token = create_test_token(secret, "user-1", &[], &[]);
    let auth = JwtAuth::new(DecodingKey::from_secret(secret), Validation::default());

    let mut ctx = test_utils::TestHttpContext::new("GET", "/")
        .with_header("authorization", &format!("Bearer  {}", token));

    let result = auth.authenticate(&mut ctx).await;
    assert!(
        result.is_ok(),
        "Token with extra whitespace after Bearer should work"
    );
    let claims = result.unwrap();
    assert!(claims.is_some());
}