#![allow(clippy::unwrap_used, clippy::expect_used)]
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as B64};
use jsonwebtoken::{DecodingKey, EncodingKey, Header};
use ppoppo_token::access_token::{AuthError, VerifyConfig, verify};
#[allow(unused_imports)]
use ppoppo_token::SharedAuthError;
use ppoppo_token::{KeySet};
use serde_json::json;
fn b64<T: serde::Serialize>(value: &T) -> String {
B64.encode(serde_json::to_vec(value).unwrap())
}
fn forge_with_header(header: &serde_json::Value, payload: &serde_json::Value) -> String {
format!("{}.{}.<sig>", b64(header), b64(payload))
}
const TEST_SUB_ULID: &str = "01HSAB00000000000000000000";
fn default_payload() -> serde_json::Value {
json!({
"iss": "https://accounts.ppoppo.com",
"sub": TEST_SUB_ULID,
"aud": "ppoppo",
"exp": 9999999999_i64,
"iat": 1_700_000_000_i64,
"jti": "01HABC00000000000000000000",
})
}
fn cfg() -> VerifyConfig {
VerifyConfig::access_token("https://accounts.ppoppo.com", "ppoppo")
}
const TEST_PRIVATE_KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIG+00IvEd4uv6IWtGFVUEBVdqnXiuI/ESQHu6rmcDvAs
-----END PRIVATE KEY-----
";
const TEST_PUBLIC_KEY_PEM: &[u8] = b"-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAh//e6j3It3xhjghg8Kpn2pM0jMCH/cvemGu4vv7D1Q4=
-----END PUBLIC KEY-----
";
const TEST_KID: &str = "k4.test.0";
fn test_keyset() -> KeySet {
let mut ks = KeySet::new();
let dec =
DecodingKey::from_ed_pem(TEST_PUBLIC_KEY_PEM).expect("test public key PEM should parse");
ks.insert(TEST_KID, dec);
ks
}
fn forge_signed_token(payload: &serde_json::Value) -> String {
let mut header = Header::new(jsonwebtoken::Algorithm::EdDSA);
header.kid = Some(TEST_KID.to_string());
header.typ = Some("at+jwt".to_string());
let enc =
EncodingKey::from_ed_pem(TEST_PRIVATE_KEY_PEM).expect("test private key PEM should parse");
jsonwebtoken::encode(&header, payload, &enc).expect("encode forge should succeed")
}
#[allow(dead_code)] fn signed_default_payload() -> serde_json::Value {
let now = time::OffsetDateTime::now_utc().unix_timestamp();
json!({
"iss": "https://accounts.ppoppo.com",
"sub": TEST_SUB_ULID,
"aud": "ppoppo",
"exp": now + 3600, "iat": now - 60, "jti": "01HABC00000000000000000000",
"client_id": "ppoppo-internal",
"cat": "access",
})
}
#[tokio::test]
async fn test_require_exp() {
let mut payload = signed_default_payload();
payload
.as_object_mut()
.expect("default payload is a JSON object")
.remove("exp");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::ExpMissing));
}
#[tokio::test]
async fn test_reject_expired() {
let mut payload = signed_default_payload();
payload["exp"] = json!(1_700_000_000_i64); let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Expired));
}
#[tokio::test]
async fn test_reject_far_future_exp() {
let mut payload = signed_default_payload();
let now = time::OffsetDateTime::now_utc().unix_timestamp();
payload["iat"] = json!(now - 60);
payload["exp"] = json!(now + 25 * 3600); let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::ExpUpperBound));
}
#[tokio::test]
async fn test_require_aud() {
let mut payload = signed_default_payload();
payload
.as_object_mut()
.expect("default payload is a JSON object")
.remove("aud");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::AudMissing));
}
#[tokio::test]
async fn test_reject_aud_mismatch() {
let mut payload = signed_default_payload();
payload["aud"] = json!("some-other-resource-server");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::AudMismatch));
}
#[tokio::test]
async fn test_aud_array_any_match() {
let mut payload = signed_default_payload();
payload["aud"] = json!(["some-other-resource", "ppoppo", "another-one"]);
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert!(
result.is_ok(),
"aud array containing cfg.audience must be accepted; got {result:?}"
);
}
#[tokio::test]
async fn test_require_iss_pinned() {
let mut payload = signed_default_payload();
payload["iss"] = json!("https://attacker.example.com");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::IssMismatch));
}
#[tokio::test]
async fn test_require_iat_in_past() {
let mut payload = signed_default_payload();
let now = time::OffsetDateTime::now_utc().unix_timestamp();
payload["iat"] = json!(now + 120); let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::IatFuture));
}
#[tokio::test]
async fn test_reject_future_iat() {
let mut payload = signed_default_payload();
let now = time::OffsetDateTime::now_utc().unix_timestamp();
payload["iat"] = json!(now + 25 * 3600); payload["exp"] = json!(now + 26 * 3600); let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::IatFuture));
}
#[tokio::test]
async fn test_reject_premature_nbf() {
let mut payload = signed_default_payload();
let now = time::OffsetDateTime::now_utc().unix_timestamp();
payload["nbf"] = json!(now + 600); let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::NotYetValid));
}
#[tokio::test]
async fn test_require_jti() {
let mut payload = signed_default_payload();
payload
.as_object_mut()
.expect("default payload is a JSON object")
.remove("jti");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::JtiMissing));
}
#[tokio::test]
async fn test_require_sub() {
let mut payload = signed_default_payload();
payload
.as_object_mut()
.expect("default payload is a JSON object")
.remove("sub");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::SubMissing));
}
#[tokio::test]
async fn test_require_client_id() {
let mut payload = signed_default_payload();
payload
.as_object_mut()
.expect("default payload is a JSON object")
.remove("client_id");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::ClientIdMissing));
}
#[tokio::test]
async fn test_reject_token_type_mismatch() {
let mut payload = signed_default_payload();
payload["cat"] = json!("refresh"); let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::TokenTypeMismatch));
}
#[tokio::test]
async fn test_reject_string_numeric_claim() {
let mut payload = signed_default_payload();
payload["exp"] = json!("9999"); let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::InvalidNumericType));
}
#[tokio::test]
async fn test_reject_pii_in_payload() {
for pii_key in ["email", "phone", "name"] {
let mut payload = signed_default_payload();
payload[pii_key] = json!("alice@example.com");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(
result,
Err(AuthError::UnknownClaim(pii_key.to_string())),
"expected UnknownClaim({pii_key:?}); got {result:?}",
);
}
}
#[tokio::test]
async fn test_reject_smuggled_claim() {
let mut payload = signed_default_payload();
payload["x_attacker_marker"] = json!(1);
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(
result,
Err(AuthError::UnknownClaim("x_attacker_marker".to_string())),
);
}
#[tokio::test]
async fn test_admin_token_band_pinned() {
let mut payload = signed_default_payload();
payload["admin"] = json!(true);
payload["active_ppnum"] = json!("20012345678");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::AdminBandRejected));
let mut payload = signed_default_payload();
payload["admin"] = json!(true);
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::AdminBandRejected));
let mut payload = signed_default_payload();
payload["admin"] = json!(true);
payload["active_ppnum"] = json!("10012345678"); let token = forge_signed_token(&payload);
assert!(verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await.is_ok());
let mut payload = signed_default_payload();
payload["active_ppnum"] = json!("20012345678");
let token = forge_signed_token(&payload);
assert!(verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await.is_ok());
}
#[tokio::test]
async fn test_reject_deep_delegation() {
let mut payload = signed_default_payload();
payload["dlg_depth"] = json!(5);
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::DlgDepthInvalid));
let mut payload = signed_default_payload();
payload["dlg_depth"] = json!(-1);
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::DlgDepthInvalid));
let mut payload = signed_default_payload();
payload["dlg_depth"] = json!("3");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::DlgDepthInvalid));
}
#[tokio::test]
async fn test_dlg_depth_at_bound_admitted() {
let mut payload = signed_default_payload();
payload["dlg_depth"] = json!(4);
let token = forge_signed_token(&payload);
assert!(verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await.is_ok());
}
#[tokio::test]
async fn test_reject_scopes_over_256() {
let mut payload = signed_default_payload();
let too_many: Vec<String> = (0..257).map(|i| format!("scope-{i}")).collect();
payload["scopes"] = json!(too_many);
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::ScopesTooLong));
}
#[tokio::test]
async fn test_scopes_at_256_admitted() {
let mut payload = signed_default_payload();
let exactly_max: Vec<String> = (0..256).map(|i| format!("scope-{i}")).collect();
payload["scopes"] = json!(exactly_max);
let token = forge_signed_token(&payload);
assert!(verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await.is_ok());
}
#[tokio::test]
async fn test_reject_scopes_non_array() {
let mut payload = signed_default_payload();
payload["scopes"] = json!("read write"); let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::ScopesShapeInvalid));
}
#[tokio::test]
async fn test_reject_caps_non_array() {
let mut payload = signed_default_payload();
payload["caps"] = json!("admin");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::CapsShapeInvalid));
let mut payload = signed_default_payload();
payload["caps"] = json!(["read", 1, "write"]);
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::CapsShapeInvalid));
let mut payload = signed_default_payload();
payload["caps"] = json!({"admin": true});
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::CapsShapeInvalid));
}
#[tokio::test]
async fn test_caps_empty_array_admitted() {
let mut payload = signed_default_payload();
payload["caps"] = json!([]);
let token = forge_signed_token(&payload);
assert!(verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await.is_ok());
}
#[tokio::test]
async fn test_reject_invalid_account_type() {
for bad in ["superuser", "Human" , ""] {
let mut payload = signed_default_payload();
payload["account_type"] = json!(bad);
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(
result,
Err(AuthError::AccountTypeInvalid),
"expected AccountTypeInvalid for value {bad:?}; got {result:?}",
);
}
}
#[tokio::test]
async fn test_account_type_whitelist_accepts_human_and_ai_agent() {
for good in ["human", "ai_agent"] {
let mut payload = signed_default_payload();
payload["account_type"] = json!(good);
let token = forge_signed_token(&payload);
assert!(
verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await.is_ok(),
"expected {good:?} to pass M40",
);
}
let payload = signed_default_payload();
let token = forge_signed_token(&payload);
assert!(
verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await.is_ok(),
"absent account_type must pass M40 (legacy admit)",
);
}
#[tokio::test]
async fn test_reject_non_ulid_sub() {
let mut payload = signed_default_payload();
payload["sub"] = json!("00000000000");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::SubFormatInvalid));
let mut payload = signed_default_payload();
payload["sub"] = json!("I1HSUB00000000000000000000"); let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::SubFormatInvalid));
let mut payload = signed_default_payload();
payload["sub"] = json!("not-a-ulid");
let token = forge_signed_token(&payload);
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::SubFormatInvalid));
}
#[tokio::test]
async fn test_reject_jws_json_serialization() {
let json_form = r#"{"payload":"eyJpc3MiOiJodHRwczovL2FjY291bnRzLnBwb3Bwby5jb20ifQ","signatures":[{"protected":"eyJhbGciOiJFZERTQSJ9","signature":"c2ln"}]}"#;
let result = verify(json_form, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::JwsJsonRejected)));
}
#[tokio::test]
async fn test_reject_duplicate_json_keys() {
let now = time::OffsetDateTime::now_utc().unix_timestamp();
let header_b64 = b64(&json!({"alg":"EdDSA","typ":"at+jwt","kid":TEST_KID}));
let raw_payload = format!(
r#"{{"iss":"https://accounts.ppoppo.com","sub":"a","sub":"b","aud":"ppoppo","exp":{},"iat":{},"jti":"01HABC00000000000000000000","client_id":"x","cat":"access"}}"#,
now + 3600,
now - 60
);
let payload_b64 = B64.encode(raw_payload.as_bytes());
let token = format!("{header_b64}.{payload_b64}.<sig>");
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::DuplicateJsonKeys)));
}
#[tokio::test]
async fn test_reject_lax_base64() {
let header_b64 = b64(&json!({"alg":"EdDSA","typ":"at+jwt","kid":TEST_KID}));
let bad_header = format!("{header_b64}+"); let token = format!("{bad_header}.payload.<sig>");
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::LaxBase64)));
}
#[tokio::test]
async fn test_reject_oversized_token() {
let mut payload = signed_default_payload();
payload["padding"] = json!("a".repeat(10_000)); let token = forge_signed_token(&payload);
assert!(token.len() > 8 * 1024, "padded token must exceed 8 KB");
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::OversizedToken)));
}
#[tokio::test]
async fn signed_forge_passes_phase1_skeleton() {
let token = forge_signed_token(&signed_default_payload());
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3, "JWS Compact MUST be 3 segments");
assert!(!parts[2].is_empty(), "signature segment must be non-empty");
let result = verify(&token, &cfg(), &test_keyset(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert!(
result.is_ok(),
"forged signed token must pass Phase 1 verify; got {result:?}"
);
}
#[tokio::test]
async fn test_reject_alg_none() {
let token = forge_with_header(
&json!({ "alg": "none", "typ": "at+jwt" }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::AlgNone)));
}
#[tokio::test]
async fn test_reject_alg_outside_whitelist() {
let token = forge_with_header(
&json!({ "alg": "FOO", "typ": "at+jwt" }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::AlgNotWhitelisted)));
}
#[tokio::test]
async fn test_reject_hs_family_with_pubkey() {
for alg in ["HS256", "HS384", "HS512"] {
let token = forge_with_header(&json!({ "alg": alg, "typ": "at+jwt" }), &default_payload());
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::AlgHmacRejected)), "alg={alg}");
}
}
#[tokio::test]
async fn test_reject_rs_family() {
for alg in ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"] {
let token = forge_with_header(&json!({ "alg": alg, "typ": "at+jwt" }), &default_payload());
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::AlgRsaRejected)), "alg={alg}");
}
}
#[tokio::test]
async fn test_reject_es_family() {
for alg in ["ES256", "ES384", "ES512"] {
let token = forge_with_header(&json!({ "alg": alg, "typ": "at+jwt" }), &default_payload());
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::AlgEcdsaRejected)), "alg={alg}");
}
}
#[tokio::test]
async fn test_reject_jku_header() {
let token = forge_with_header(
&json!({ "alg": "EdDSA", "typ": "at+jwt", "jku": "https://attacker.example/keys" }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::HeaderJku)));
}
#[tokio::test]
async fn test_reject_x5u_header() {
let token = forge_with_header(
&json!({ "alg": "EdDSA", "typ": "at+jwt", "x5u": "https://attacker.example/cert.pem" }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::HeaderX5u)));
}
#[tokio::test]
async fn test_reject_jwk_header() {
let token = forge_with_header(
&json!({
"alg": "EdDSA", "typ": "at+jwt",
"jwk": { "kty": "OKP", "crv": "Ed25519", "x": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }
}),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::HeaderJwk)));
}
#[tokio::test]
async fn test_reject_x5c_header() {
let token = forge_with_header(
&json!({
"alg": "EdDSA", "typ": "at+jwt",
"x5c": ["MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA"]
}),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::HeaderX5c)));
}
#[tokio::test]
async fn test_reject_unknown_crit() {
let token = forge_with_header(
&json!({ "alg": "EdDSA", "typ": "at+jwt", "crit": ["b64"] }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::HeaderCrit)));
}
#[tokio::test]
async fn test_reject_jwe_payload() {
let jwe = "header.encrypted_key.iv.ciphertext.tag";
let result = verify(jwe, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::JwePayload)));
}
#[tokio::test]
async fn test_reject_extra_header_params() {
let token = forge_with_header(
&json!({ "alg": "EdDSA", "typ": "at+jwt", "kid": "k4.pid.x", "x_attacker_marker": 1 }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::HeaderExtraParam)));
}
#[tokio::test]
async fn test_reject_b64_false_header() {
let token = forge_with_header(
&json!({ "alg": "EdDSA", "typ": "at+jwt", "b64": false }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::HeaderB64False)));
}
#[tokio::test]
async fn test_reject_nested_jws() {
for cty in ["JWT", "jwt", "JOSE", "jose"] {
let token = forge_with_header(
&json!({ "alg": "EdDSA", "typ": "at+jwt", "cty": cty }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::NestedJws)), "cty={cty}");
}
}
#[tokio::test]
async fn test_reject_arbitrary_typ() {
for bad_typ in ["JWT", "jwt", "application/jwt", "id_token+jwt", ""] {
let token = forge_with_header(
&json!({ "alg": "EdDSA", "typ": bad_typ }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::TypMismatch)), "typ={bad_typ:?}");
}
let no_typ = forge_with_header(&json!({ "alg": "EdDSA" }), &default_payload());
assert_eq!(
verify(&no_typ, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await,
Err(AuthError::Jose(SharedAuthError::TypMismatch)),
);
}
#[tokio::test]
async fn test_reject_id_token_at_access_endpoint() {
let token = forge_with_header(
&json!({ "alg": "EdDSA", "typ": "id_token+jwt" }),
&default_payload(),
);
let result = verify(&token, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(result, Err(AuthError::Jose(SharedAuthError::TypMismatch)));
}
#[tokio::test]
async fn test_reject_unknown_kid() {
let no_kid = forge_with_header(
&json!({ "alg": "EdDSA", "typ": "at+jwt" }),
&default_payload(),
);
assert_eq!(
verify(&no_kid, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await,
Err(AuthError::Jose(SharedAuthError::KidUnknown)),
);
let unknown_kid = forge_with_header(
&json!({ "alg": "EdDSA", "typ": "at+jwt", "kid": "k4.pid.attacker" }),
&default_payload(),
);
assert_eq!(
verify(&unknown_kid, &cfg(), &KeySet::new(), time::OffsetDateTime::now_utc().unix_timestamp()).await,
Err(AuthError::Jose(SharedAuthError::KidUnknown)),
);
}