#![cfg(feature = "axum")]
use axum::{
body::Body,
http::{header, Request, StatusCode},
middleware::from_fn_with_state,
routing::post,
Router,
};
use backbone_auth::org::{org_auth, OrgIssuer, OrgVerifier};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use serde::Serialize;
use std::time::Duration;
use tower::ServiceExt;
use uuid::Uuid;
const SECRET: &[u8] = b"org-guard-framework-test-secret";
const WRONG_SECRET: &[u8] = b"not-the-signing-secret";
const NOT_EXPIRED: usize = 9_999_999_999;
const EXPIRED: usize = 1_000_000_000;
#[test]
fn issuer_access_token_round_trips_through_the_verifier() {
let issuer = OrgIssuer::hs256(SECRET);
let verifier = OrgVerifier::hs256(SECRET);
let user = Uuid::new_v4();
let acting = Uuid::new_v4();
let sister = Uuid::new_v4();
let token = issuer
.issue_access(&user.to_string(), acting, &[sister], None, Duration::from_secs(3600))
.unwrap();
let ctx = verifier.verify(&token).expect("minted access token must verify");
assert_eq!(ctx.acting_unit_id, acting);
assert_eq!(ctx.entitled_units, vec![sister]);
assert_eq!(ctx.user_id, user.to_string());
assert_eq!(ctx.legacy_company_id, None);
}
#[test]
fn issuer_refresh_token_is_refused_by_the_verifier() {
let issuer = OrgIssuer::hs256(SECRET);
let verifier = OrgVerifier::hs256(SECRET);
let refresh = issuer
.issue_refresh("user-1", Uuid::new_v4(), &[], None, Duration::from_secs(7 * 24 * 3600))
.unwrap();
assert!(verifier.verify(&refresh).is_none());
}
#[test]
fn a_twin_claim_mint_passes_both_guards() {
use backbone_auth::company::CompanyVerifier;
let issuer = OrgIssuer::hs256(SECRET);
let org_verifier = OrgVerifier::hs256(SECRET);
let company_verifier = CompanyVerifier::hs256(SECRET);
let user = Uuid::new_v4();
let company = Uuid::new_v4();
let access = issuer
.issue_access(&user.to_string(), company, &[], Some(company), Duration::from_secs(3600))
.unwrap();
let org_ctx = org_verifier
.verify(&access)
.expect("the org guard accepts the twin-claim token");
assert_eq!(org_ctx.acting_unit_id, company);
assert_eq!(org_ctx.legacy_company_id, Some(company));
let company_ctx = company_verifier
.verify(&access)
.expect("the company guard accepts the same token during the transition");
assert_eq!(company_ctx.company_id, company);
assert_eq!(company_ctx.user_id, user.to_string());
let refresh = issuer
.issue_refresh(&user.to_string(), company, &[], Some(company), Duration::from_secs(7 * 24 * 3600))
.unwrap();
assert!(org_verifier.verify(&refresh).is_none());
assert!(company_verifier.verify(&refresh).is_none());
}
#[test]
fn an_org_only_mint_is_refused_by_the_company_guard() {
use backbone_auth::company::CompanyVerifier;
let issuer = OrgIssuer::hs256(SECRET);
let company_verifier = CompanyVerifier::hs256(SECRET);
let org_only = issuer
.issue_access("user-1", Uuid::new_v4(), &[], None, Duration::from_secs(3600))
.unwrap();
assert!(company_verifier.verify(&org_only).is_none());
}
#[test]
fn a_token_signed_by_a_different_secret_is_refused() {
let issuer = OrgIssuer::hs256(WRONG_SECRET);
let verifier = OrgVerifier::hs256(SECRET);
let token = issuer
.issue_access("user-1", Uuid::new_v4(), &[], None, Duration::from_secs(3600))
.unwrap();
assert!(verifier.verify(&token).is_none());
}
#[derive(Serialize)]
struct TestClaims {
sub: String,
exp: usize,
#[serde(skip_serializing_if = "Option::is_none")]
org_unit_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
typ: Option<String>,
}
fn token_with(
secret: &[u8],
exp: usize,
org_unit_id: Option<Uuid>,
typ: Option<String>,
) -> String {
let claims = TestClaims { sub: "user-1".into(), exp, org_unit_id, typ };
encode(&Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(secret)).unwrap()
}
fn app() -> Router {
Router::new()
.route("/guarded", post(|| async { StatusCode::OK }))
.layer(from_fn_with_state(OrgVerifier::hs256(SECRET), org_auth))
}
async fn call(bearer: Option<&str>) -> axum::response::Response {
let mut req = Request::builder().method("POST").uri("/guarded");
if let Some(t) = bearer {
req = req.header(header::AUTHORIZATION, format!("Bearer {t}"));
}
app().oneshot(req.body(Body::empty()).unwrap()).await.unwrap()
}
#[tokio::test]
async fn og1_unauthenticated_request_is_rejected() {
assert_eq!(call(None).await.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn og2_token_without_org_unit_id_is_rejected() {
let t = token_with(SECRET, NOT_EXPIRED, None, None);
assert_eq!(call(Some(&t)).await.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn og3_expired_token_is_rejected() {
let t = token_with(SECRET, EXPIRED, Some(Uuid::new_v4()), None);
assert_eq!(call(Some(&t)).await.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn og4_token_signed_with_wrong_secret_is_rejected() {
let t = token_with(WRONG_SECRET, NOT_EXPIRED, Some(Uuid::new_v4()), None);
assert_eq!(call(Some(&t)).await.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn og5_refresh_typed_token_is_rejected() {
let refresh = OrgIssuer::hs256(SECRET)
.issue_refresh("user-1", Uuid::new_v4(), &[], None, Duration::from_secs(7 * 24 * 3600))
.unwrap();
assert_eq!(call(Some(&refresh)).await.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn og6_unknown_typed_token_is_rejected() {
let t = token_with(SECRET, NOT_EXPIRED, Some(Uuid::new_v4()), Some("godmode".into()));
assert_eq!(call(Some(&t)).await.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn og7_typless_legacy_token_reaches_the_pool_check() {
let t = token_with(SECRET, NOT_EXPIRED, Some(Uuid::new_v4()), None);
assert_eq!(call(Some(&t)).await.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn og8_missing_tenant_pool_is_a_wiring_error_not_a_pass() {
let issuer = OrgIssuer::hs256(SECRET);
let access = issuer
.issue_access("user-1", Uuid::new_v4(), &[], None, Duration::from_secs(3600))
.unwrap();
assert_eq!(call(Some(&access)).await.status(), StatusCode::INTERNAL_SERVER_ERROR);
}