use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use serde_json::{Value, json};
use tower::ServiceExt;
use vta_service::test_support::{TestAppContext, build_test_app};
use vti_common::auth::session::{
Session, SessionState, now_epoch, store_refresh_index, store_session,
};
async fn seed_admin_acl(ctx: &TestAppContext, did: &str) {
let entry = vti_common::acl::AclEntry::new(did, vti_common::acl::Role::Admin, "test")
.with_created_at(1);
vti_common::acl::store_acl_entry(&ctx.acl_ks, &entry)
.await
.expect("seed admin ACL");
}
async fn seed_authenticated_session(
ctx: &TestAppContext,
did: &str,
refresh_token: &str,
) -> String {
let session_id = format!("sess-{}", uuid::Uuid::new_v4());
let session = Session {
session_id: session_id.clone(),
did: did.to_string(),
challenge: String::new(),
state: SessionState::Authenticated,
created_at: now_epoch(),
last_seen: now_epoch(),
refresh_token: Some(refresh_token.to_string()),
refresh_expires_at: Some(now_epoch() + 86_400),
tee_attested: false,
amr: vec!["did".into()],
acr: "aal1".into(),
acr_expires_at: None,
token_id: None,
session_pubkey_b58btc: None,
};
store_session(&ctx.sessions_ks, &session)
.await
.expect("store session");
store_refresh_index(&ctx.sessions_ks, refresh_token, &session_id)
.await
.expect("store refresh index");
session_id
}
fn refresh_doc(refresh_token: &str) -> Vec<u8> {
json!({
"id": "urn:uuid:refresh-itest-1",
"type": "https://trusttasks.org/spec/auth/refresh/0.1",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"issuer": "did:key:z6MkRefresher",
"recipient": "did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD",
"payload": { "refreshToken": refresh_token },
})
.to_string()
.into_bytes()
}
fn post(uri: &str, body: Vec<u8>) -> Request<Body> {
Request::builder()
.method("POST")
.uri(uri)
.header("content-type", "application/json")
.header("x-forwarded-for", "203.0.113.9")
.body(Body::from(body))
.unwrap()
}
async fn send(router: &axum::Router, req: Request<Body>) -> (StatusCode, Value) {
let resp = router.clone().oneshot(req).await.expect("request");
let status = resp.status();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let v: Value = serde_json::from_slice(&bytes)
.unwrap_or_else(|_| json!({"raw": String::from_utf8_lossy(&bytes).to_string()}));
(status, v)
}
#[tokio::test]
async fn trust_task_refresh_rotates_tokens() {
let (router, ctx) = build_test_app().await;
let did = "did:key:z6MkRefresher";
let old_token = "refresh-tok-itest-aaaa";
seed_admin_acl(&ctx, did).await;
seed_authenticated_session(&ctx, did, old_token).await;
let (status, body) = send(&router, post("/auth/refresh", refresh_doc(old_token))).await;
assert_eq!(
status,
StatusCode::OK,
"Trust Task refresh must succeed: {body}"
);
assert!(
body["type"]
.as_str()
.is_some_and(|t| t.ends_with("/auth/refresh/0.1#response")),
"response is a TT #response doc: {body}"
);
assert_eq!(body["payload"]["session"]["subject"], did, "{body}");
assert!(
body["payload"]["tokens"]["accessToken"]
.as_str()
.is_some_and(|t| !t.is_empty()),
"a fresh access token is issued: {body}"
);
let new_token = body["payload"]["tokens"]["refreshToken"]
.as_str()
.expect("rotated refresh token");
assert_ne!(new_token, old_token, "refresh token must rotate: {body}");
let (replay_status, replay_body) =
send(&router, post("/auth/refresh", refresh_doc(old_token))).await;
assert_eq!(
replay_status,
StatusCode::OK,
"an immediate retry is a lost response, not a compromise: {replay_body}"
);
assert_eq!(
replay_body["payload"]["tokens"]["refreshToken"]
.as_str()
.expect("replayed refresh token"),
new_token,
"the retry must replay the same token, never mint a second live one",
);
}
#[tokio::test]
async fn trust_task_refresh_rejects_unknown_token() {
let (router, _ctx) = build_test_app().await;
let (status, _) = send(
&router,
post("/auth/refresh", refresh_doc("refresh-tok-never-issued")),
)
.await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"an unknown refresh token must be rejected"
);
}
fn rotated_token(body: &Value) -> String {
body["payload"]["tokens"]["refreshToken"]
.as_str()
.unwrap_or_else(|| panic!("rotated refresh token in {body}"))
.to_string()
}
#[tokio::test]
async fn refresh_token_reuse_is_detected_and_revokes_the_whole_session() {
let (router, ctx) = build_test_app().await;
let did = "did:key:z6MkRefresher";
let stolen = "refresh-tok-itest-reuse";
seed_admin_acl(&ctx, did).await;
seed_authenticated_session(&ctx, did, stolen).await;
let (s1, b1) = send(&router, post("/auth/refresh", refresh_doc(stolen))).await;
assert_eq!(s1, StatusCode::OK, "{b1}");
let second = rotated_token(&b1);
let (s2, b2) = send(&router, post("/auth/refresh", refresh_doc(&second))).await;
assert_eq!(s2, StatusCode::OK, "{b2}");
let live = rotated_token(&b2);
let (replay, _) = send(&router, post("/auth/refresh", refresh_doc(stolen))).await;
assert_eq!(
replay,
StatusCode::UNAUTHORIZED,
"a replayed refresh token must be refused",
);
let (after, _) = send(&router, post("/auth/refresh", refresh_doc(&live))).await;
assert_eq!(
after,
StatusCode::UNAUTHORIZED,
"detection must revoke the session, killing the still-live token too",
);
}
#[tokio::test]
async fn an_immediate_refresh_retry_replays_the_same_tokens_and_keeps_the_session() {
let (router, ctx) = build_test_app().await;
let did = "did:key:z6MkRefresher";
let token = "refresh-tok-itest-retry";
seed_admin_acl(&ctx, did).await;
seed_authenticated_session(&ctx, did, token).await;
let (s1, lost) = send(&router, post("/auth/refresh", refresh_doc(token))).await;
assert_eq!(s1, StatusCode::OK, "{lost}");
let (s2, retried) = send(&router, post("/auth/refresh", refresh_doc(token))).await;
assert_eq!(
s2,
StatusCode::OK,
"a retry inside the grace window must succeed: {retried}",
);
assert_eq!(
rotated_token(&retried),
rotated_token(&lost),
"the retry must replay the same pair, not rotate again",
);
let (s3, b3) = send(
&router,
post("/auth/refresh", refresh_doc(&rotated_token(&retried))),
)
.await;
assert_eq!(s3, StatusCode::OK, "the client stays signed in: {b3}");
}
#[tokio::test]
async fn an_unknown_token_does_not_revoke_a_live_session() {
let (router, ctx) = build_test_app().await;
let did = "did:key:z6MkRefresher";
let token = "refresh-tok-itest-bystander";
seed_admin_acl(&ctx, did).await;
seed_authenticated_session(&ctx, did, token).await;
let (status, _) = send(
&router,
post("/auth/refresh", refresh_doc("refresh-tok-never-issued")),
)
.await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
let (after, body) = send(&router, post("/auth/refresh", refresh_doc(token))).await;
assert_eq!(
after,
StatusCode::OK,
"an unrecognised token must not disturb a live session: {body}",
);
}