use bytes::Bytes;
use http::{HeaderMap, HeaderValue, Method, StatusCode};
use parlov_core::{
always_applicable, DifferentialSet, NormativeStrength, OracleClass, ProbeDefinition,
ProbeExchange, ResponseSurface, SignalSurface, Technique, Vector,
};
use proptest::prelude::*;
use crate::aggregation::auth_classifier::classify_auth_block;
use crate::aggregation::auth_equivalence::auth_gate_decision;
use crate::aggregation::auth_equivalence::equivalent_auth_block;
use crate::aggregation::auth_parsers::{parse_auth_error_body, parse_www_authenticate};
use crate::aggregation::auth_types::{
AuthBlockConfidence, AuthBlockFamily, AuthGateDecision, AuthScheme, CredentialBlockKind,
};
use crate::aggregation::login_redirect::is_login_redirect;
use crate::aggregation::precondition::{AuthBlockLayer, PreconditionBlock};
fn req(headers: HeaderMap) -> ProbeDefinition {
ProbeDefinition {
url: "https://example.com/r/1".into(),
method: Method::GET,
headers,
body: None,
}
}
fn req_with_auth() -> ProbeDefinition {
let mut h = HeaderMap::new();
h.insert(
http::header::AUTHORIZATION,
HeaderValue::from_static("Bearer x"),
);
req(h)
}
fn res(status: u16, headers: HeaderMap, body: &'static [u8]) -> ResponseSurface {
ResponseSurface {
status: StatusCode::from_u16(status).expect("valid status"),
headers,
body: Bytes::from_static(body),
timing_ns: 0,
}
}
fn res_with_body_bytes(status: u16, headers: HeaderMap, body: Bytes) -> ResponseSurface {
ResponseSurface {
status: StatusCode::from_u16(status).expect("valid status"),
headers,
body,
timing_ns: 0,
}
}
fn json_headers() -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
h
}
fn www_auth(value: &'static str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(
http::header::WWW_AUTHENTICATE,
HeaderValue::from_static(value),
);
h
}
fn proxy_auth(value: &'static str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(
http::header::PROXY_AUTHENTICATE,
HeaderValue::from_static(value),
);
h
}
fn location_headers(loc: &'static str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(http::header::LOCATION, HeaderValue::from_static(loc));
h
}
fn technique() -> Technique {
Technique {
id: "test",
name: "Test",
oracle_class: OracleClass::Existence,
vector: Vector::StatusCodeDiff,
strength: NormativeStrength::Must,
normalization_weight: Some(0.2),
inverted_signal_weight: None,
method_relevant: false,
parser_relevant: false,
applicability: always_applicable,
contradiction_surface: SignalSurface::Status,
}
}
#[allow(clippy::similar_names)]
fn diff_set(
b_req: ProbeDefinition,
b_res: ResponseSurface,
p_req: ProbeDefinition,
p_res: ResponseSurface,
) -> DifferentialSet {
DifferentialSet {
baseline: vec![ProbeExchange {
request: b_req,
response: b_res,
}],
probe: vec![ProbeExchange {
request: p_req,
response: p_res,
}],
canonical: None,
technique: technique(),
}
}
#[test]
fn parse_bearer_with_realm() {
let c = parse_www_authenticate(r#"Bearer realm="api""#).expect("parses");
assert_eq!(c.scheme, AuthScheme::Bearer);
assert_eq!(c.realm.as_deref(), Some("api"));
assert!(c.error.is_none());
}
#[test]
fn parse_bearer_with_realm_and_invalid_token_error() {
let c = parse_www_authenticate(r#"Bearer realm="api", error="invalid_token""#).expect("parses");
assert_eq!(c.scheme, AuthScheme::Bearer);
assert_eq!(c.realm.as_deref(), Some("api"));
assert_eq!(c.error.as_deref(), Some("invalid_token"));
}
#[test]
fn parse_bearer_with_insufficient_scope_and_scope_param() {
let c = parse_www_authenticate(r#"Bearer error="insufficient_scope", scope="user.read""#)
.expect("parses");
assert_eq!(c.scheme, AuthScheme::Bearer);
assert_eq!(c.error.as_deref(), Some("insufficient_scope"));
assert_eq!(c.scope.as_deref(), Some("user.read"));
}
#[test]
fn parse_basic_with_realm() {
let c = parse_www_authenticate(r#"Basic realm="api""#).expect("parses");
assert_eq!(c.scheme, AuthScheme::Basic);
assert_eq!(c.realm.as_deref(), Some("api"));
}
#[test]
fn parse_digest_with_realm_and_nonce() {
let c = parse_www_authenticate(r#"Digest realm="api", nonce="abc123""#).expect("parses");
assert_eq!(c.scheme, AuthScheme::Digest);
assert_eq!(c.realm.as_deref(), Some("api"));
}
#[test]
fn parse_empty_input_returns_none() {
assert!(parse_www_authenticate("").is_none());
assert!(parse_www_authenticate(" ").is_none());
}
#[test]
fn parse_garbage_unrecognised_scheme_is_other() {
let c = parse_www_authenticate("WeirdScheme thing").expect("parses (scheme=Other)");
assert_eq!(c.scheme, AuthScheme::Other);
}
#[test]
fn parse_multiple_challenges_first_wins() {
let c = parse_www_authenticate(r#"Bearer realm="primary", Basic realm="secondary""#)
.expect("parses");
assert_eq!(c.scheme, AuthScheme::Bearer);
assert_eq!(c.realm.as_deref(), Some("primary"));
}
#[test]
fn parse_error_description_quoted() {
let c = parse_www_authenticate(
r#"Bearer error="invalid_token", error_description="The token has expired""#,
)
.expect("parses");
assert_eq!(c.error.as_deref(), Some("invalid_token"));
assert_eq!(
c.error_description.as_deref(),
Some("The token has expired")
);
}
#[test]
fn parse_json_invalid_token_strong() {
let body = Bytes::from_static(br#"{"error":"invalid_token"}"#);
let sig = parse_auth_error_body(Some("application/json"), &body).expect("parses");
assert_eq!(sig.code, "invalid_token");
assert_eq!(sig.confidence, AuthBlockConfidence::Strong);
}
#[test]
fn parse_json_unauthorized_medium() {
let body = Bytes::from_static(br#"{"error":"unauthorized"}"#);
let sig = parse_auth_error_body(Some("application/json"), &body).expect("parses");
assert_eq!(sig.code, "unauthorized");
assert_eq!(sig.confidence, AuthBlockConfidence::Medium);
}
#[test]
fn parse_json_forbidden_weak() {
let body = Bytes::from_static(br#"{"code":"forbidden"}"#);
let sig = parse_auth_error_body(Some("application/json"), &body).expect("parses");
assert_eq!(sig.code, "forbidden");
assert_eq!(sig.confidence, AuthBlockConfidence::Weak);
}
#[test]
fn parse_html_content_type_skipped() {
let body = Bytes::from_static(br#"{"error":"invalid_token"}"#);
assert!(parse_auth_error_body(Some("text/html"), &body).is_none());
}
#[test]
fn parse_body_over_8_kib_skipped() {
let large = Bytes::from(vec![b'x'; 9 * 1024]);
assert!(parse_auth_error_body(Some("application/json"), &large).is_none());
}
#[test]
fn parse_empty_body_returns_none() {
let body = Bytes::new();
assert!(parse_auth_error_body(Some("application/json"), &body).is_none());
}
#[test]
fn parse_malformed_json_returns_none() {
let body = Bytes::from_static(b"not json");
assert!(parse_auth_error_body(Some("application/json"), &body).is_none());
}
#[test]
fn parse_problem_json_invalid_token() {
let body = Bytes::from_static(br#"{"error":"invalid_token"}"#);
let sig = parse_auth_error_body(Some("application/problem+json"), &body)
.expect("problem+json parsed");
assert_eq!(sig.code, "invalid_token");
}
#[test]
fn parse_form_urlencoded_invalid_token() {
let body = Bytes::from_static(b"error=invalid_token&state=xyz");
let sig = parse_auth_error_body(Some("application/x-www-form-urlencoded"), &body)
.expect("form parses");
assert_eq!(sig.code, "invalid_token");
}
#[test]
fn login_redirect_basic_login_path() {
let r = res(302, location_headers("/login"), b"");
let sig = is_login_redirect(&r).expect("matches");
assert_eq!(sig.confidence, AuthBlockConfidence::Medium);
}
#[test]
fn login_redirect_oauth_authorize_with_params_strong() {
let r = res(
302,
location_headers("/oauth/authorize?client_id=abc&redirect_uri=https://x/cb"),
b"",
);
let sig = is_login_redirect(&r).expect("matches");
assert_eq!(sig.confidence, AuthBlockConfidence::Strong);
}
#[test]
fn login_redirect_with_session_cookie_strong() {
let mut h = location_headers("/login");
h.insert(
http::header::SET_COOKIE,
HeaderValue::from_static("session=abc; HttpOnly"),
);
let r = res(302, h, b"");
let sig = is_login_redirect(&r).expect("matches");
assert_eq!(sig.confidence, AuthBlockConfidence::Strong);
}
#[test]
fn login_redirect_non_login_path_returns_none() {
let r = res(302, location_headers("/api/users/2"), b"");
assert!(is_login_redirect(&r).is_none());
}
#[test]
fn login_redirect_200_with_login_location_returns_none() {
let r = res(200, location_headers("/login"), b"");
assert!(is_login_redirect(&r).is_none());
}
#[test]
fn login_redirect_303_signin() {
let r = res(303, location_headers("/signin"), b"");
assert!(is_login_redirect(&r).is_some());
}
#[test]
fn classify_401_with_bearer_no_auth_strong_no_credential() {
let request = req(HeaderMap::new());
let response = res(401, www_auth(r#"Bearer realm="api""#), b"");
let sig = classify_auth_block(&request, &response).expect("classifies");
assert_eq!(sig.family, AuthBlockFamily::OriginAuthentication);
assert_eq!(sig.confidence, AuthBlockConfidence::Strong);
assert_eq!(sig.credential_state, CredentialBlockKind::NoCredential);
}
#[test]
fn classify_401_with_invalid_token_credential_rejected() {
let request = req_with_auth();
let response = res(401, www_auth(r#"Bearer error="invalid_token""#), b"");
let sig = classify_auth_block(&request, &response).expect("classifies");
assert_eq!(
sig.credential_state,
CredentialBlockKind::CredentialRejected
);
}
#[test]
fn classify_401_with_insufficient_scope_insufficient_scope() {
let request = req_with_auth();
let response = res(401, www_auth(r#"Bearer error="insufficient_scope""#), b"");
let sig = classify_auth_block(&request, &response).expect("classifies");
assert_eq!(sig.credential_state, CredentialBlockKind::InsufficientScope);
}
#[test]
fn classify_401_without_www_authenticate_medium() {
let request = req(HeaderMap::new());
let response = res(401, HeaderMap::new(), b"");
let sig = classify_auth_block(&request, &response).expect("classifies");
assert_eq!(sig.family, AuthBlockFamily::OriginAuthentication);
assert_eq!(sig.confidence, AuthBlockConfidence::Medium);
}
#[test]
fn classify_407_with_proxy_auth() {
let request = req(HeaderMap::new());
let response = res(407, proxy_auth("Basic realm=\"corp\""), b"");
let sig = classify_auth_block(&request, &response).expect("classifies");
assert_eq!(sig.family, AuthBlockFamily::ProxyAuthentication);
}
#[test]
fn classify_511_network_authentication() {
let request = req(HeaderMap::new());
let response = res(511, HeaderMap::new(), b"");
let sig = classify_auth_block(&request, &response).expect("classifies");
assert_eq!(sig.family, AuthBlockFamily::NetworkAuthentication);
assert_eq!(sig.credential_state, CredentialBlockKind::NotApplicable);
}
#[test]
fn classify_200_with_www_authenticate_medium() {
let request = req(HeaderMap::new());
let response = res(200, www_auth(r#"Bearer realm="api""#), b"");
let sig = classify_auth_block(&request, &response).expect("classifies");
assert_eq!(sig.family, AuthBlockFamily::OriginAuthentication);
assert_eq!(sig.confidence, AuthBlockConfidence::Medium);
}
#[test]
fn classify_200_with_invalid_token_envelope_auth_error_envelope() {
let request = req(HeaderMap::new());
let response = res(200, json_headers(), br#"{"error":"invalid_token"}"#);
let sig = classify_auth_block(&request, &response).expect("classifies");
assert_eq!(sig.family, AuthBlockFamily::AuthErrorEnvelope);
}
#[test]
fn classify_403_with_insufficient_scope_origin_authorization() {
let request = req_with_auth();
let response = res(403, www_auth(r#"Bearer error="insufficient_scope""#), b"");
let sig = classify_auth_block(&request, &response).expect("classifies");
assert_eq!(sig.family, AuthBlockFamily::OriginAuthorization);
assert_eq!(sig.credential_state, CredentialBlockKind::InsufficientScope);
}
#[test]
fn classify_403_without_auth_signal_returns_none() {
let request = req_with_auth();
let response = res(403, HeaderMap::new(), b"");
assert!(classify_auth_block(&request, &response).is_none());
}
#[test]
fn classify_403_with_auth_present_no_challenge_returns_none() {
let request = req_with_auth();
let response = res(403, HeaderMap::new(), br#"{"error":"forbidden"}"#);
assert!(classify_auth_block(&request, &response).is_none());
}
#[test]
fn classify_200_normal_response_returns_none() {
let request = req(HeaderMap::new());
let response = res(200, HeaderMap::new(), br#"{"data":"ok"}"#);
assert!(classify_auth_block(&request, &response).is_none());
}
#[test]
fn equivalent_two_identical_401_bearer() {
let request = req(HeaderMap::new());
let r1 = res(401, www_auth(r#"Bearer realm="api""#), b"unauth");
let r2 = res(401, www_auth(r#"Bearer realm="api""#), b"unauth");
let s1 = classify_auth_block(&request, &r1).unwrap();
let s2 = classify_auth_block(&request, &r2).unwrap();
assert!(equivalent_auth_block(&s1, &s2, &r1, &r2));
}
#[test]
fn not_equivalent_different_error_param() {
let request = req_with_auth();
let r1 = res(401, www_auth(r#"Bearer error="invalid_token""#), b"");
let r2 = res(401, www_auth(r#"Bearer error="insufficient_scope""#), b"");
let s1 = classify_auth_block(&request, &r1).unwrap();
let s2 = classify_auth_block(&request, &r2).unwrap();
assert!(!equivalent_auth_block(&s1, &s2, &r1, &r2));
}
#[test]
fn not_equivalent_different_realm() {
let request = req(HeaderMap::new());
let r1 = res(401, www_auth(r#"Bearer realm="api""#), b"");
let r2 = res(401, www_auth(r#"Bearer realm="admin""#), b"");
let s1 = classify_auth_block(&request, &r1).unwrap();
let s2 = classify_auth_block(&request, &r2).unwrap();
assert!(!equivalent_auth_block(&s1, &s2, &r1, &r2));
}
#[test]
fn not_equivalent_401_vs_403() {
let r401 = res(401, www_auth(r#"Bearer realm="api""#), b"");
let r403 = res(403, www_auth(r#"Bearer error="insufficient_scope""#), b"");
let s1 = classify_auth_block(&req(HeaderMap::new()), &r401).unwrap();
let s2 = classify_auth_block(&req_with_auth(), &r403).unwrap();
assert!(!equivalent_auth_block(&s1, &s2, &r401, &r403));
}
#[test]
fn equivalent_two_403_insufficient_scope_same_body() {
let request = req_with_auth();
let r1 = res(
403,
www_auth(r#"Bearer error="insufficient_scope""#),
b"denied",
);
let r2 = res(
403,
www_auth(r#"Bearer error="insufficient_scope""#),
b"denied",
);
let s1 = classify_auth_block(&request, &r1).unwrap();
let s2 = classify_auth_block(&request, &r2).unwrap();
assert!(equivalent_auth_block(&s1, &s2, &r1, &r2));
}
#[test]
fn equivalent_two_401_same_parsed_code_different_bytes() {
let request = req(HeaderMap::new());
let mut json_h = json_headers();
json_h.insert(
http::header::WWW_AUTHENTICATE,
HeaderValue::from_static(r#"Bearer realm="api""#),
);
let r1 = res_with_body_bytes(
401,
json_h.clone(),
Bytes::from_static(br#"{"error":"invalid_token"}"#),
);
let r2 = res_with_body_bytes(
401,
json_h,
Bytes::from_static(br#"{"error":"invalid_token","timestamp":1}"#),
);
let s1 = classify_auth_block(&request, &r1).unwrap();
let s2 = classify_auth_block(&request, &r2).unwrap();
assert!(equivalent_auth_block(&s1, &s2, &r1, &r2));
}
#[test]
fn equivalent_two_login_redirects_same_target() {
let request = req(HeaderMap::new());
let r1 = res(302, location_headers("/login"), b"");
let r2 = res(302, location_headers("/login"), b"");
let s1 = classify_auth_block(&request, &r1).unwrap();
let s2 = classify_auth_block(&request, &r2).unwrap();
assert!(equivalent_auth_block(&s1, &s2, &r1, &r2));
}
#[test]
fn not_equivalent_login_redirects_different_targets() {
let request = req(HeaderMap::new());
let r1 = res(302, location_headers("/login?next=/a"), b"");
let r2 = res(302, location_headers("/login?next=/b"), b"");
let s1 = classify_auth_block(&request, &r1).unwrap();
let s2 = classify_auth_block(&request, &r2).unwrap();
assert!(!equivalent_auth_block(&s1, &s2, &r1, &r2));
}
#[test]
fn gate_when_two_equivalent_401() {
let ds = diff_set(
req(HeaderMap::new()),
res(401, www_auth(r#"Bearer realm="api""#), b"unauth"),
req(HeaderMap::new()),
res(401, www_auth(r#"Bearer realm="api""#), b"unauth"),
);
match auth_gate_decision(&ds) {
AuthGateDecision::Gate(PreconditionBlock::AuthGateBeforeTechnique {
credential_state,
layer,
}) => {
assert_eq!(credential_state, CredentialBlockKind::NoCredential);
assert_eq!(layer, AuthBlockLayer::Origin);
}
d => panic!("expected Gate, got {d:?}"),
}
}
#[test]
fn do_not_gate_status_differential_401_vs_404() {
let ds = diff_set(
req(HeaderMap::new()),
res(401, www_auth(r#"Bearer realm="api""#), b""),
req(HeaderMap::new()),
res(404, HeaderMap::new(), b""),
);
assert_eq!(auth_gate_decision(&ds), AuthGateDecision::DoNotGate);
}
#[test]
fn do_not_gate_status_differential_401_vs_200() {
let ds = diff_set(
req(HeaderMap::new()),
res(401, www_auth(r#"Bearer realm="api""#), b""),
req(HeaderMap::new()),
res(200, HeaderMap::new(), b""),
);
assert_eq!(auth_gate_decision(&ds), AuthGateDecision::DoNotGate);
}
#[test]
fn do_not_gate_403_vs_404_idor_pattern() {
let ds = diff_set(
req_with_auth(),
res(403, HeaderMap::new(), b""),
req_with_auth(),
res(404, HeaderMap::new(), b""),
);
assert_eq!(auth_gate_decision(&ds), AuthGateDecision::NoAuthInvolvement);
}
#[test]
fn no_auth_involvement_200_200() {
let ds = diff_set(
req(HeaderMap::new()),
res(200, HeaderMap::new(), b""),
req(HeaderMap::new()),
res(200, HeaderMap::new(), b""),
);
assert_eq!(auth_gate_decision(&ds), AuthGateDecision::NoAuthInvolvement);
}
#[test]
fn do_not_gate_invalid_token_vs_insufficient_scope() {
let ds = diff_set(
req_with_auth(),
res(401, www_auth(r#"Bearer error="invalid_token""#), b""),
req_with_auth(),
res(401, www_auth(r#"Bearer error="insufficient_scope""#), b""),
);
assert_eq!(auth_gate_decision(&ds), AuthGateDecision::DoNotGate);
}
#[test]
fn gate_two_407_proxy() {
let ds = diff_set(
req(HeaderMap::new()),
res(407, proxy_auth(r#"Basic realm="corp""#), b""),
req(HeaderMap::new()),
res(407, proxy_auth(r#"Basic realm="corp""#), b""),
);
match auth_gate_decision(&ds) {
AuthGateDecision::Gate(PreconditionBlock::AuthGateBeforeTechnique { layer, .. }) => {
assert_eq!(layer, AuthBlockLayer::Proxy);
}
d => panic!("expected Gate(Proxy), got {d:?}"),
}
}
#[test]
fn gate_two_511_network() {
let ds = diff_set(
req(HeaderMap::new()),
res(511, HeaderMap::new(), b""),
req(HeaderMap::new()),
res(511, HeaderMap::new(), b""),
);
match auth_gate_decision(&ds) {
AuthGateDecision::Gate(PreconditionBlock::AuthGateBeforeTechnique {
layer,
credential_state,
}) => {
assert_eq!(layer, AuthBlockLayer::Network);
assert_eq!(credential_state, CredentialBlockKind::NotApplicable);
}
d => panic!("expected Gate(Network), got {d:?}"),
}
}
proptest! {
#[test]
fn equivalent_reflexive(auth_present in any::<bool>()) {
let request = if auth_present { req_with_auth() } else { req(HeaderMap::new()) };
let response = res(401, www_auth(r#"Bearer realm="api""#), b"x");
let sig = classify_auth_block(&request, &response).unwrap();
prop_assert!(equivalent_auth_block(&sig, &sig, &response, &response));
}
#[test]
fn classify_referentially_transparent(status in 200u16..=599) {
let s = if http::StatusCode::from_u16(status).is_err() { 404 } else { status };
let request = req(HeaderMap::new());
let response = res(s, HeaderMap::new(), b"");
let s1 = classify_auth_block(&request, &response);
let s2 = classify_auth_block(&request, &response);
prop_assert_eq!(s1, s2);
}
#[test]
fn decision_referentially_transparent(b_status in 200u16..=599, p_status in 200u16..=599) {
let bs = if http::StatusCode::from_u16(b_status).is_err() { 404 } else { b_status };
let ps = if http::StatusCode::from_u16(p_status).is_err() { 404 } else { p_status };
let ds = diff_set(
req(HeaderMap::new()),
res(bs, HeaderMap::new(), b""),
req(HeaderMap::new()),
res(ps, HeaderMap::new(), b""),
);
let d1 = auth_gate_decision(&ds);
let d2 = auth_gate_decision(&ds);
prop_assert_eq!(d1, d2);
}
#[test]
fn status_differentials_never_gated(
b_status in 200u16..=599,
p_status in 200u16..=599,
) {
let bs = if http::StatusCode::from_u16(b_status).is_err() { 404 } else { b_status };
let ps = if http::StatusCode::from_u16(p_status).is_err() { 404 } else { p_status };
prop_assume!(bs != ps);
let ds = diff_set(
req(HeaderMap::new()),
res(bs, www_auth(r#"Bearer realm="api""#), b""),
req(HeaderMap::new()),
res(ps, www_auth(r#"Bearer realm="api""#), b""),
);
let d = auth_gate_decision(&ds);
prop_assert!(
!matches!(d, AuthGateDecision::Gate(_)),
"status {bs} vs {ps} produced Gate (must be DoNotGate or NoAuthInvolvement)",
);
}
}