openauth-plugins 0.0.4

Official OpenAuth plugin modules.
Documentation
use super::common::{
    cookie_header_from_response, merge_cookie_headers, multi_cookie_name, response_token,
    set_cookie_values, Fixture,
};
use http::{Method, StatusCode};
use openauth_core::session::DbSessionStore;
use openauth_plugins::multi_session::MultiSessionConfig;
use serde_json::Value;

#[tokio::test]
async fn sign_in_sets_signed_multi_session_cookie() -> Result<(), Box<dyn std::error::Error>> {
    let fixture = Fixture::new(MultiSessionConfig::default()).await?;

    let response = fixture
        .sign_in("ada@example.com", "secret123", None)
        .await?;
    let cookies = set_cookie_values(&response);
    let token = response_token(&response)?;
    let multi_cookie_name = multi_cookie_name(&token);

    assert!(cookies
        .iter()
        .any(|cookie| cookie.starts_with(&format!("{multi_cookie_name}={token}."))));
    Ok(())
}

#[tokio::test]
async fn sign_up_sets_signed_multi_session_cookie() -> Result<(), Box<dyn std::error::Error>> {
    let fixture = Fixture::new(MultiSessionConfig::default()).await?;

    let response = fixture.sign_up("new-user@example.com", None).await?;
    let cookies = set_cookie_values(&response);
    let token = response_token(&response)?;

    assert_eq!(response.status(), StatusCode::OK);
    assert!(cookies
        .iter()
        .any(|cookie| cookie.starts_with(&format!("{}={token}.", multi_cookie_name(&token)))));
    Ok(())
}

#[tokio::test]
async fn latest_sign_in_becomes_active_session() -> Result<(), Box<dyn std::error::Error>> {
    let fixture = Fixture::new(MultiSessionConfig::default()).await?;
    let first = fixture
        .sign_in("ada@example.com", "secret123", None)
        .await?;
    let second = fixture
        .sign_in(
            "grace@example.com",
            "secret123",
            Some(&cookie_header_from_response(&first)),
        )
        .await?;
    let cookie = merge_cookie_headers(&[
        &cookie_header_from_response(&first),
        &cookie_header_from_response(&second),
    ]);

    let response = fixture
        .request(Method::GET, "/api/auth/get-session", "", Some(&cookie))
        .await?;
    let body: Value = serde_json::from_slice(response.body())?;

    assert_eq!(body["user"]["email"], "grace@example.com");
    Ok(())
}

#[tokio::test]
async fn sign_out_revokes_only_signed_multi_session_cookies(
) -> Result<(), Box<dyn std::error::Error>> {
    let fixture = Fixture::new(MultiSessionConfig::default()).await?;
    let attacker = fixture
        .sign_in("ada@example.com", "secret123", None)
        .await?;
    let victim = fixture
        .sign_in("grace@example.com", "secret123", None)
        .await?;
    let victim_token = response_token(&victim)?;
    let forged_cookie = format!(
        "{}={victim_token}.fake-signature",
        multi_cookie_name(&victim_token)
    );
    let attacker_cookie = format!(
        "{}; {forged_cookie}",
        cookie_header_from_response(&attacker)
    );

    let response = fixture
        .request(
            Method::POST,
            "/api/auth/sign-out",
            "{}",
            Some(&attacker_cookie),
        )
        .await?;

    assert_eq!(response.status(), StatusCode::OK);
    assert!(DbSessionStore::new(fixture.adapter.as_ref())
        .find_session(&victim_token)
        .await?
        .is_some());
    Ok(())
}

#[tokio::test]
async fn sign_out_revokes_valid_multi_session_cookies_and_list_returns_empty(
) -> Result<(), Box<dyn std::error::Error>> {
    let fixture = Fixture::new(MultiSessionConfig::default()).await?;
    let first = fixture
        .sign_in("ada@example.com", "secret123", None)
        .await?;
    let second = fixture
        .sign_in(
            "grace@example.com",
            "secret123",
            Some(&cookie_header_from_response(&first)),
        )
        .await?;
    let cookie = merge_cookie_headers(&[
        &cookie_header_from_response(&first),
        &cookie_header_from_response(&second),
    ]);

    let sign_out = fixture
        .request(Method::POST, "/api/auth/sign-out", "{}", Some(&cookie))
        .await?;
    let response_cookie = cookie_header_from_response(&sign_out);
    let list = fixture
        .request(
            Method::GET,
            "/api/auth/multi-session/list-device-sessions",
            "",
            Some(&merge_cookie_headers(&[&cookie, &response_cookie])),
        )
        .await?;
    let body: Value = serde_json::from_slice(list.body())?;

    assert_eq!(sign_out.status(), StatusCode::OK);
    assert_eq!(body.as_array().map(Vec::len), Some(0));
    Ok(())
}

#[tokio::test]
async fn same_user_sign_in_replaces_old_multi_session_cookie(
) -> Result<(), Box<dyn std::error::Error>> {
    let fixture = Fixture::new(MultiSessionConfig::default()).await?;
    let first = fixture
        .sign_in("ada@example.com", "secret123", None)
        .await?;
    let first_token = response_token(&first)?;
    let second = fixture
        .sign_in(
            "ada@example.com",
            "secret123",
            Some(&cookie_header_from_response(&first)),
        )
        .await?;
    let second_token = response_token(&second)?;

    assert_ne!(first_token, second_token);
    assert!(set_cookie_values(&second).iter().any(|cookie| {
        cookie.starts_with(&format!("{}=;", multi_cookie_name(&first_token)))
            && cookie.contains("Max-Age=0")
    }));
    assert!(DbSessionStore::new(fixture.adapter.as_ref())
        .find_session(&first_token)
        .await?
        .is_none());
    Ok(())
}

#[tokio::test]
async fn maximum_sessions_prevents_adding_extra_multi_session_cookie(
) -> Result<(), Box<dyn std::error::Error>> {
    let fixture = Fixture::new(MultiSessionConfig {
        maximum_sessions: 1,
    })
    .await?;
    let first = fixture
        .sign_in("ada@example.com", "secret123", None)
        .await?;
    let second = fixture
        .sign_in(
            "grace@example.com",
            "secret123",
            Some(&cookie_header_from_response(&first)),
        )
        .await?;
    let second_token = response_token(&second)?;

    assert!(!set_cookie_values(&second)
        .iter()
        .any(|cookie| cookie.starts_with(&format!("{}=", multi_cookie_name(&second_token)))));
    Ok(())
}