//! Cross-language pin: Rust HS256 JWT + MWT must be byte-identical to the //! Python core (`auth/jwt.py`, `mwt.py`, both PyJWT-backed). //! //! Byte-identity is the whole point — Edge and the origin cache key on these //! tokens, so a one-byte divergence is a cache-key spoof surface. The Python //! reference is the oracle: it mints with fixed claims + a fixed `iat`/`exp` //! (we pin `now` on both sides) and the Rust token must match exactly. We also //! prove round-trip: Rust decodes a Python-minted token and vice-versa. use mizan_core::{ create_access_token, create_mwt, create_refresh_token, decode_jwt, decode_mwt, compute_permission_key, JwtConfig, }; use std::path::PathBuf; use std::process::Command; fn afi_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../tests/afi") .canonicalize() .expect("tests/afi exists") } fn py(code: &str) -> String { let out = Command::new("uv") .args(["run", "python", "-c", code]) .current_dir(afi_dir()) .output() .expect("invoke uv run python"); assert!( out.status.success(), "python reference failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr), ); String::from_utf8(out.stdout).unwrap().trim().to_string() } const NOW: i64 = 1_700_000_000; #[test] fn jwt_access_token_matches_python() { let cfg = JwtConfig::new("jwt-pin-secret"); let rust = create_access_token(&cfg, "42", "sess-abc", true, false, NOW); // Python: freeze time to NOW, mint an access token with the same claims. let code = format!( "import time; from unittest import mock; \ from mizan_core.auth.jwt import JWTConfig, create_access_token; \ cfg = JWTConfig(private_key='jwt-pin-secret', public_key='jwt-pin-secret'); \ orig = time.time; \ time.time = lambda: {NOW}; \ print(create_access_token('42', 'sess-abc', cfg, is_staff=True, is_superuser=False)); \ time.time = orig", ); let expected = py(&code); assert_eq!(rust, expected, "JWT access-token byte mismatch"); } #[test] fn jwt_refresh_token_matches_python() { let cfg = JwtConfig::new("jwt-pin-secret"); let rust = create_refresh_token(&cfg, "7", "sid-9", false, true, NOW); let code = format!( "import time; from mizan_core.auth.jwt import JWTConfig, create_refresh_token; \ cfg = JWTConfig(private_key='jwt-pin-secret', public_key='jwt-pin-secret'); \ time.time = lambda: {NOW}; \ print(create_refresh_token('7', 'sid-9', cfg, is_staff=False, is_superuser=True))", ); assert_eq!(rust, py(&code), "JWT refresh-token byte mismatch"); } #[test] fn jwt_roundtrip_decode_python_minted() { // A Python-minted access token must decode in Rust with matching claims. let code = format!( "import time; from mizan_core.auth.jwt import JWTConfig, create_access_token; \ cfg = JWTConfig(private_key='rt-secret', public_key='rt-secret'); \ time.time = lambda: {NOW}; \ print(create_access_token('99', 'sess-x', cfg, is_staff=False, is_superuser=True))", ); let token = py(&code); let cfg = JwtConfig::new("rt-secret"); let payload = decode_jwt(&token, &cfg, Some("access"), NOW + 10).expect("decodes"); assert_eq!(payload.sub, "99"); assert_eq!(payload.sid, "sess-x"); assert!(payload.superuser); assert!(!payload.staff); // Wrong secret → None; expired → None. assert!(decode_jwt(&token, &JwtConfig::new("nope"), None, NOW + 10).is_none()); assert!(decode_jwt(&token, &cfg, Some("access"), NOW + 10_000).is_none()); // Type mismatch → None. assert!(decode_jwt(&token, &cfg, Some("refresh"), NOW + 10).is_none()); } #[test] fn permission_key_matches_python() { let perms = vec!["app.add_thing".to_string(), "app.view_thing".to_string()]; let rust = compute_permission_key(true, false, &perms); let code = "from mizan_core.mwt import compute_permission_key; \ from unittest.mock import MagicMock; \ u = MagicMock(); u.is_staff=True; u.is_superuser=False; \ u.get_all_permissions = MagicMock(return_value={'app.view_thing','app.add_thing'}); \ print(compute_permission_key(u))"; assert_eq!(rust, py(code), "pkey byte mismatch"); } #[test] fn mwt_matches_python() { // Build the same pkey on both sides, then mint with frozen time + fixed // kid/audience and compare bytes. let perms = vec!["app.view_thing".to_string()]; let pkey = compute_permission_key(false, false, &perms); let rust = create_mwt("mwt-pin-secret", "5", false, false, &pkey, 300, "mizan", "v1", NOW); let code = format!( "import time; from unittest.mock import MagicMock; \ from mizan_core.mwt import create_mwt; \ u = MagicMock(); u.pk=5; u.is_staff=False; u.is_superuser=False; \ u.get_all_permissions = MagicMock(return_value={{'app.view_thing'}}); \ time.time = lambda: {NOW}; \ print(create_mwt(u, 'mwt-pin-secret', ttl=300, audience='mizan', kid='v1'))", ); assert_eq!(rust, py(&code), "MWT byte mismatch"); } #[test] fn mwt_roundtrip_and_rejections() { let pkey = compute_permission_key(true, true, &[]); let token = create_mwt("rt-mwt", "13", true, true, &pkey, 300, "mizan", "v1", NOW); let p = decode_mwt(&token, "rt-mwt", "mizan", NOW + 5).expect("decodes"); assert_eq!(p.sub, "13"); assert!(p.staff && p.superuser); assert_eq!(p.kid, "v1"); assert_eq!(p.pkey.len(), 64); // Wrong secret, wrong audience, expired → None. assert!(decode_mwt(&token, "wrong", "mizan", NOW + 5).is_none()); assert!(decode_mwt(&token, "rt-mwt", "other", NOW + 5).is_none()); assert!(decode_mwt(&token, "rt-mwt", "mizan", NOW + 10_000).is_none()); // And a Python-minted MWT decodes in Rust. let code = format!( "import time; from unittest.mock import MagicMock; from mizan_core.mwt import create_mwt; \ u = MagicMock(); u.pk=21; u.is_staff=True; u.is_superuser=False; \ u.get_all_permissions = MagicMock(return_value=set()); \ time.time = lambda: {NOW}; print(create_mwt(u, 'rt-mwt', ttl=300, audience='mizan', kid='v1'))", ); let py_token = py(&code); let pp = decode_mwt(&py_token, "rt-mwt", "mizan", NOW + 5).expect("py mwt decodes in rust"); assert_eq!(pp.sub, "21"); assert!(pp.staff && !pp.superuser); }