rustauth-core 0.2.0

Core types and primitives for RustAuth.
Documentation
use serde::{Deserialize, Serialize};

use rustauth_core::cookies::{
    delete_session_cookie, get_cookie_cache, get_cookies, set_cookie_cache, set_session_cookie,
    CookieCachePayload, CookieCacheStrategy, SessionCookieOptions,
};
#[cfg(feature = "jose")]
use rustauth_core::crypto::SecretConfig;
use rustauth_core::options::{CookieCacheOptions, RustAuthOptions, SessionOptions};

fn dev_options() -> RustAuthOptions {
    crate::common::with_test_defaults(RustAuthOptions::default())
}

#[test]
fn set_session_cookie_signs_session_token() -> Result<(), Box<dyn std::error::Error>> {
    let options = dev_options().secret("secret-a-at-least-32-chars-long!!".to_owned());
    let cookies = get_cookies(&options)?;

    let set = set_session_cookie(
        &cookies,
        "secret-a-at-least-32-chars-long!!",
        "session-token",
        SessionCookieOptions::default(),
    )?;

    assert_eq!(set[0].name, "rustauth.session_token");
    assert_ne!(set[0].value, "session-token");
    assert!(set[0].value.starts_with("session-token."));
    Ok(())
}

#[test]
fn set_session_cookie_omits_max_age_when_dont_remember_is_true(
) -> Result<(), Box<dyn std::error::Error>> {
    let cookies = get_cookies(&dev_options())?;

    let set = set_session_cookie(
        &cookies,
        "secret-a-at-least-32-chars-long!!",
        "session-token",
        SessionCookieOptions {
            dont_remember: true,
            ..SessionCookieOptions::default()
        },
    )?;

    let session_cookie = set
        .iter()
        .find(|cookie| cookie.name == "rustauth.session_token")
        .ok_or("session cookie")?;
    let remember_cookie = set
        .iter()
        .find(|cookie| cookie.name == "rustauth.dont_remember")
        .ok_or("remember cookie")?;

    assert_eq!(session_cookie.attributes.max_age, None);
    assert!(remember_cookie.value.starts_with("true."));
    Ok(())
}

#[test]
fn delete_session_cookie_expires_session_cookies_and_chunks(
) -> Result<(), Box<dyn std::error::Error>> {
    let cookies = get_cookies(&dev_options())?;

    let expired = delete_session_cookie(
        &cookies,
        "rustauth.session_data.0=abc; rustauth.session_data.1=def; rustauth.account_data.0=ghi; rustauth.account_data.1=jkl",
        false,
    );

    assert!(expired
        .iter()
        .any(|cookie| cookie.name == "rustauth.session_token"));
    assert!(expired
        .iter()
        .any(|cookie| cookie.name == "rustauth.session_data"));
    assert!(expired
        .iter()
        .any(|cookie| cookie.name == "rustauth.session_data.0"));
    assert!(expired
        .iter()
        .any(|cookie| cookie.name == "rustauth.account_data"));
    assert!(expired
        .iter()
        .any(|cookie| cookie.name == "rustauth.account_data.0"));
    assert!(expired
        .iter()
        .any(|cookie| cookie.name == "rustauth.dont_remember"));
    assert!(expired
        .iter()
        .all(|cookie| cookie.attributes.max_age == Some(0)));
    Ok(())
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct TestSession {
    token: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct TestUser {
    id: String,
}

#[test]
fn compact_cookie_cache_round_trips_with_valid_signature() -> Result<(), Box<dyn std::error::Error>>
{
    let options = RustAuthOptions {
        secret: Some("secret-a-at-least-32-chars-long!!".to_owned()),
        session: SessionOptions {
            cookie_cache: CookieCacheOptions {
                enabled: true,
                version: Some("v1".to_owned()),
                ..CookieCacheOptions::default()
            },
            ..SessionOptions::default()
        },
        ..RustAuthOptions::default()
    };
    let cookies = get_cookies(&options)?;
    let payload = CookieCachePayload {
        session: TestSession {
            token: "session-token".to_owned(),
        },
        user: TestUser {
            id: "user_123".to_owned(),
        },
        updated_at: 100,
        version: "v1".to_owned(),
    };

    let set = set_cookie_cache(
        &cookies,
        "secret-a-at-least-32-chars-long!!",
        &payload,
        CookieCacheStrategy::Compact,
        300,
    )?;
    let header = format!("{}={}", set[0].name, set[0].value);
    let decoded = get_cookie_cache::<TestSession, TestUser>(
        &header,
        &cookies.session_data.name,
        "secret-a-at-least-32-chars-long!!",
        CookieCacheStrategy::Compact,
        Some("v1"),
    )?;

    assert_eq!(decoded, Some(payload));
    Ok(())
}

#[test]
fn compact_cookie_cache_rejects_tampered_payload() -> Result<(), Box<dyn std::error::Error>> {
    let cookies = get_cookies(&dev_options())?;
    let payload = CookieCachePayload {
        session: TestSession {
            token: "session-token".to_owned(),
        },
        user: TestUser {
            id: "user_123".to_owned(),
        },
        updated_at: 100,
        version: "v1".to_owned(),
    };

    let set = set_cookie_cache(
        &cookies,
        "secret-a-at-least-32-chars-long!!",
        &payload,
        CookieCacheStrategy::Compact,
        300,
    )?;
    let mut value = set[0].value.clone();
    value.push('a');
    let header = format!("{}={}", set[0].name, value);
    let decoded = get_cookie_cache::<TestSession, TestUser>(
        &header,
        &cookies.session_data.name,
        "secret-a-at-least-32-chars-long!!",
        CookieCacheStrategy::Compact,
        Some("v1"),
    )?;

    assert_eq!(decoded, None);
    Ok(())
}

#[test]
fn jwt_cookie_cache_round_trips_with_valid_signature() -> Result<(), Box<dyn std::error::Error>> {
    let cookies = get_cookies(&dev_options())?;
    let payload = CookieCachePayload {
        session: TestSession {
            token: "session-token".to_owned(),
        },
        user: TestUser {
            id: "user_123".to_owned(),
        },
        updated_at: 100,
        version: "v1".to_owned(),
    };

    let set = set_cookie_cache(
        &cookies,
        "secret-a-at-least-32-chars-long!!",
        &payload,
        CookieCacheStrategy::Jwt,
        300,
    )?;
    let header = format!("{}={}", set[0].name, set[0].value);
    let decoded = get_cookie_cache::<TestSession, TestUser>(
        &header,
        &cookies.session_data.name,
        "secret-a-at-least-32-chars-long!!",
        CookieCacheStrategy::Jwt,
        Some("v1"),
    )?;

    assert_eq!(decoded, Some(payload));
    Ok(())
}

#[test]
fn jwt_cookie_cache_rejects_wrong_secret() -> Result<(), Box<dyn std::error::Error>> {
    let cookies = get_cookies(&dev_options())?;
    let payload = CookieCachePayload {
        session: TestSession {
            token: "session-token".to_owned(),
        },
        user: TestUser {
            id: "user_123".to_owned(),
        },
        updated_at: 100,
        version: "v1".to_owned(),
    };

    let set = set_cookie_cache(
        &cookies,
        "secret-a-at-least-32-chars-long!!",
        &payload,
        CookieCacheStrategy::Jwt,
        300,
    )?;
    let header = format!("{}={}", set[0].name, set[0].value);
    let decoded = get_cookie_cache::<TestSession, TestUser>(
        &header,
        &cookies.session_data.name,
        "secret-b-at-least-32-chars-long!!",
        CookieCacheStrategy::Jwt,
        Some("v1"),
    )?;

    assert_eq!(decoded, None);
    Ok(())
}

#[test]
fn jwt_cookie_cache_rejects_version_mismatch() -> Result<(), Box<dyn std::error::Error>> {
    let cookies = get_cookies(&dev_options())?;
    let payload = CookieCachePayload {
        session: TestSession {
            token: "session-token".to_owned(),
        },
        user: TestUser {
            id: "user_123".to_owned(),
        },
        updated_at: 100,
        version: "v1".to_owned(),
    };

    let set = set_cookie_cache(
        &cookies,
        "secret-a-at-least-32-chars-long!!",
        &payload,
        CookieCacheStrategy::Jwt,
        300,
    )?;
    let header = format!("{}={}", set[0].name, set[0].value);
    let decoded = get_cookie_cache::<TestSession, TestUser>(
        &header,
        &cookies.session_data.name,
        "secret-a-at-least-32-chars-long!!",
        CookieCacheStrategy::Jwt,
        Some("v2"),
    )?;

    assert_eq!(decoded, None);
    Ok(())
}

#[test]
#[cfg(feature = "jose")]
fn jwe_cookie_cache_round_trips_with_valid_secret() -> Result<(), Box<dyn std::error::Error>> {
    let cookies = get_cookies(&dev_options())?;
    let payload = CookieCachePayload {
        session: TestSession {
            token: "session-token".to_owned(),
        },
        user: TestUser {
            id: "user_123".to_owned(),
        },
        updated_at: 100,
        version: "v1".to_owned(),
    };

    let set = set_cookie_cache(
        &cookies,
        "secret-a-at-least-32-chars-long!!",
        &payload,
        CookieCacheStrategy::Jwe,
        300,
    )?;
    let header = format!("{}={}", set[0].name, set[0].value);
    let decoded = get_cookie_cache::<TestSession, TestUser>(
        &header,
        &cookies.session_data.name,
        "secret-a-at-least-32-chars-long!!",
        CookieCacheStrategy::Jwe,
        Some("v1"),
    )?;

    assert_eq!(decoded, Some(payload));
    Ok(())
}

#[test]
#[cfg(feature = "jose")]
fn jwe_cookie_cache_rejects_wrong_secret() -> Result<(), Box<dyn std::error::Error>> {
    let cookies = get_cookies(&dev_options())?;
    let payload = CookieCachePayload {
        session: TestSession {
            token: "session-token".to_owned(),
        },
        user: TestUser {
            id: "user_123".to_owned(),
        },
        updated_at: 100,
        version: "v1".to_owned(),
    };

    let set = set_cookie_cache(
        &cookies,
        "secret-a-at-least-32-chars-long!!",
        &payload,
        CookieCacheStrategy::Jwe,
        300,
    )?;
    let header = format!("{}={}", set[0].name, set[0].value);
    let decoded = get_cookie_cache::<TestSession, TestUser>(
        &header,
        &cookies.session_data.name,
        "secret-b-at-least-32-chars-long!!",
        CookieCacheStrategy::Jwe,
        Some("v1"),
    )?;

    assert_eq!(decoded, None);
    Ok(())
}

#[test]
#[cfg(feature = "jose")]
fn jwe_cookie_cache_decodes_with_rotated_secret_config() -> Result<(), Box<dyn std::error::Error>> {
    let cookies = get_cookies(&dev_options())?;
    let payload = CookieCachePayload {
        session: TestSession {
            token: "session-token".to_owned(),
        },
        user: TestUser {
            id: "user_123".to_owned(),
        },
        updated_at: 100,
        version: "v1".to_owned(),
    };
    let old_config = SecretConfig::new([(1, "secret-a-at-least-32-chars-long!!")]);
    let rotated_config = SecretConfig::new([
        (2, "secret-b-at-least-32-chars-long!!"),
        (1, "secret-a-at-least-32-chars-long!!"),
    ]);

    let set = set_cookie_cache(
        &cookies,
        &old_config,
        &payload,
        CookieCacheStrategy::Jwe,
        300,
    )?;
    let header = format!("{}={}", set[0].name, set[0].value);
    let decoded = get_cookie_cache::<TestSession, TestUser>(
        &header,
        &cookies.session_data.name,
        &rotated_config,
        CookieCacheStrategy::Jwe,
        Some("v1"),
    )?;

    assert_eq!(decoded, Some(payload));
    Ok(())
}