use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64;
use http_body_util::BodyExt;
use tokio::sync::RwLock;
use tower::ServiceExt;
use vti_common::auth::extractor::ADMIN_SESSION_COOKIE;
use vti_common::auth::jwt::JwtKeys;
use vti_common::auth::session::{Session, SessionState, now_epoch, store_session};
use vti_common::config::StoreConfig;
use vti_common::store::Store;
use vtc_service::acl::{VtcAclEntry, VtcRole, store_acl_entry};
use vtc_service::config::AppConfig;
use vtc_service::routes;
use vtc_service::server::AppState;
const ADMIN_DID: &str = "did:key:z6MkAdminCookie";
const ACL_TRUST_TASK: &str = "https://trusttasks.org/openvtc/vtc/acl/legacy/manage/1.0";
fn init_jwt_provider() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = jsonwebtoken::crypto::aws_lc::DEFAULT_PROVIDER.install_default();
});
}
struct Fixture {
router: axum::Router,
state: AppState,
jwt_keys: Arc<JwtKeys>,
_dir: tempfile::TempDir,
}
async fn build_fixture() -> Fixture {
init_jwt_provider();
let dir = tempfile::tempdir().expect("tempdir");
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.expect("open store");
let sessions_ks = store.keyspace("sessions").unwrap();
let acl_ks = store.keyspace("acl").unwrap();
let community_ks = store.keyspace("community").unwrap();
let config_ks = store.keyspace("config").unwrap();
let passkey_ks = store.keyspace("passkey").unwrap();
let install_ks = store.keyspace("install").unwrap();
let members_ks = store.keyspace("members").unwrap();
let join_requests_ks = store.keyspace("join_requests").unwrap();
let policies_ks = store.keyspace("policies").unwrap();
let active_policies_ks = store.keyspace("active_policies").unwrap();
let status_lists_ks = store.keyspace("status_lists").unwrap();
let registry_records_ks = store.keyspace("registry_records").unwrap();
let sync_queue_ks = store.keyspace("sync_queue").unwrap();
let sync_cursor_ks = store.keyspace("sync_cursor").unwrap();
let relationships_ks = store.keyspace("relationships").unwrap();
let relationships_by_did_ks = store.keyspace("relationships_by_did").unwrap();
let endorsement_types_ks = store.keyspace("endorsement_types").unwrap();
let endorsements_ks = store.keyspace("endorsements").unwrap();
let audit_ks = store.keyspace("audit").unwrap();
let audit_key_ks = store.keyspace("audit_key").unwrap();
let jwt_seed = [0x42u8; 32];
let jwt_keys = Arc::new(JwtKeys::from_ed25519_bytes(&jwt_seed, "VTC").expect("jwt keys"));
let config: AppConfig = toml::from_str(&format!(
r#"
vtc_did = "did:key:z6MkTestVTC"
[store]
data_dir = "{}"
[auth]
jwt_signing_key = "{}"
"#,
dir.path().display(),
BASE64.encode(jwt_seed),
))
.expect("parse config");
let state = AppState {
sessions_ks: sessions_ks.clone(),
acl_ks: acl_ks.clone(),
community_ks,
config_ks,
passkey_ks,
install_ks: install_ks.clone(),
members_ks: members_ks.clone(),
join_requests_ks: join_requests_ks.clone(),
policies_ks: policies_ks.clone(),
active_policies_ks: active_policies_ks.clone(),
status_lists_ks: status_lists_ks.clone(),
registry_records_ks: registry_records_ks.clone(),
sync_queue_ks: sync_queue_ks.clone(),
sync_cursor_ks: sync_cursor_ks.clone(),
relationships_ks: relationships_ks.clone(),
relationships_by_did_ks: relationships_by_did_ks.clone(),
endorsement_types_ks: endorsement_types_ks.clone(),
endorsements_ks: endorsements_ks.clone(),
registry_client: None,
registry_health: vtc_service::registry::RegistryHealth::new(),
credential_signer: None,
config: Arc::new(RwLock::new(config)),
did_resolver: None,
secrets_resolver: None,
jwt_keys: Some(jwt_keys.clone()),
atm: None,
webauthn: None,
public_url: None,
install_signer: None,
install_store: vtc_service::install::InstallTokenStore::new(install_ks),
audit_ks,
audit_key_ks,
audit_writer: None,
shutdown_tx: tokio::sync::watch::channel(false).0,
supervisor: None,
};
store_acl_entry(
&state.acl_ks,
&VtcAclEntry {
did: ADMIN_DID.into(),
role: VtcRole::Admin,
label: None,
allowed_contexts: vec![],
created_at: now_epoch(),
created_by: "test".into(),
expires_at: None,
},
)
.await
.expect("acl insert");
let router = routes::router().with_state(state.clone());
Fixture {
router,
state,
jwt_keys,
_dir: dir,
}
}
async fn mint_session(fix: &Fixture, audience: &str) -> String {
let session_id = format!("sess-{}", uuid::Uuid::new_v4());
store_session(
&fix.state.sessions_ks,
&Session {
session_id: session_id.clone(),
did: ADMIN_DID.into(),
challenge: "test".into(),
state: SessionState::Authenticated,
created_at: now_epoch(),
refresh_token: None,
refresh_expires_at: None,
tee_attested: false,
amr: Vec::new(),
acr: String::new(),
token_id: None,
session_pubkey_b58btc: None,
},
)
.await
.expect("store session");
let keys = if audience == "VTC" {
fix.jwt_keys.clone()
} else {
Arc::new(JwtKeys::from_ed25519_bytes(&[0x42u8; 32], audience).unwrap())
};
let claims = keys.new_claims(
ADMIN_DID.to_string(),
session_id,
"admin".to_string(),
vec![],
900,
false,
);
keys.encode(&claims).expect("encode")
}
async fn request(router: &axum::Router, req: Request<Body>) -> (StatusCode, String) {
let resp = router.clone().oneshot(req).await.expect("request");
let status = resp.status();
let body = resp.into_body().collect().await.unwrap().to_bytes();
(status, String::from_utf8_lossy(&body).into_owned())
}
#[tokio::test]
async fn admin_cookie_authenticates_protected_route() {
let fix = build_fixture().await;
let jwt = mint_session(&fix, "VTC").await;
let req = Request::builder()
.method("GET")
.uri("/v1/acl")
.header("Trust-Task", ACL_TRUST_TASK)
.header("Cookie", format!("{ADMIN_SESSION_COOKIE}={jwt}"))
.body(Body::empty())
.unwrap();
let (status, _body) = request(&fix.router, req).await;
assert_eq!(
status,
StatusCode::OK,
"cookie-bearing request must authenticate the admin route"
);
}
#[tokio::test]
async fn wrong_cookie_name_returns_401() {
let fix = build_fixture().await;
let jwt = mint_session(&fix, "VTC").await;
let req = Request::builder()
.method("GET")
.uri("/v1/acl")
.header("Trust-Task", ACL_TRUST_TASK)
.header("Cookie", format!("session={jwt}; other=foo"))
.body(Body::empty())
.unwrap();
let (status, _body) = request(&fix.router, req).await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn cookie_alongside_other_cookies_authenticates() {
let fix = build_fixture().await;
let jwt = mint_session(&fix, "VTC").await;
let req = Request::builder()
.method("GET")
.uri("/v1/acl")
.header("Trust-Task", ACL_TRUST_TASK)
.header(
"Cookie",
format!("csrf=abc123; {ADMIN_SESSION_COOKIE}={jwt}; analytics=enabled"),
)
.body(Body::empty())
.unwrap();
let (status, _body) = request(&fix.router, req).await;
assert_eq!(status, StatusCode::OK);
}
#[tokio::test]
async fn bearer_takes_precedence_over_cookie() {
let fix = build_fixture().await;
let valid_bearer = mint_session(&fix, "VTC").await;
let foreign_cookie = mint_session(&fix, "EVIL-AUD").await;
let req = Request::builder()
.method("GET")
.uri("/v1/acl")
.header("Trust-Task", ACL_TRUST_TASK)
.header("Authorization", format!("Bearer {valid_bearer}"))
.header("Cookie", format!("{ADMIN_SESSION_COOKIE}={foreign_cookie}"))
.body(Body::empty())
.unwrap();
let (status, _body) = request(&fix.router, req).await;
assert_eq!(
status,
StatusCode::OK,
"Bearer header takes precedence; cookie ignored when bearer is present"
);
}
#[tokio::test]
async fn foreign_audience_cookie_rejected() {
let fix = build_fixture().await;
let foreign = mint_session(&fix, "EVIL-AUD").await;
let req = Request::builder()
.method("GET")
.uri("/v1/acl")
.header("Trust-Task", ACL_TRUST_TASK)
.header("Cookie", format!("{ADMIN_SESSION_COOKIE}={foreign}"))
.body(Body::empty())
.unwrap();
let (status, _body) = request(&fix.router, req).await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
}