use parlov_core::{ProbeDefinition, RequestAuthState, ResponseSurface};
use super::auth_parsers::{parse_auth_error_body, parse_www_authenticate};
use super::auth_types::{
AuthBlockConfidence, AuthBlockFamily, AuthBlockSignature, AuthChallenge, AuthErrorBodySignal,
CredentialBlockKind,
};
use super::login_redirect::is_login_redirect;
#[allow(clippy::similar_names)] #[must_use]
pub fn classify_auth_block(
req: &ProbeDefinition,
res: &ResponseSurface,
) -> Option<AuthBlockSignature> {
let status = res.status.as_u16();
let auth_state = RequestAuthState::from_request(req);
if status == 401 {
return Some(classify_401(res, auth_state));
}
if status == 407 {
return Some(classify_407(res));
}
if status == 511 {
return Some(classify_511());
}
if status == 403 {
return classify_403_auth_block(req, res);
}
if let Some(sig) = classify_www_authenticate_non_401(res, auth_state) {
return Some(sig);
}
if let Some(sig) = classify_login_redirect(res) {
return Some(sig);
}
classify_body_envelope(res)
}
fn classify_401(res: &ResponseSurface, auth_state: RequestAuthState) -> AuthBlockSignature {
let challenge = parse_challenge_header(res, http::header::WWW_AUTHENTICATE);
let confidence = strong_if_present(challenge.as_ref());
let credential_state = credential_state_from_challenge(auth_state, challenge.as_ref());
AuthBlockSignature {
family: AuthBlockFamily::OriginAuthentication,
confidence,
status: 401,
credential_state,
challenge,
body_signal: parse_body_signal(res),
login_redirect: None,
}
}
fn classify_407(res: &ResponseSurface) -> AuthBlockSignature {
let challenge = parse_challenge_header(res, http::header::PROXY_AUTHENTICATE);
let confidence = strong_if_present(challenge.as_ref());
AuthBlockSignature {
family: AuthBlockFamily::ProxyAuthentication,
confidence,
status: 407,
credential_state: CredentialBlockKind::NoCredential,
challenge,
body_signal: parse_body_signal(res),
login_redirect: None,
}
}
fn classify_511() -> AuthBlockSignature {
AuthBlockSignature {
family: AuthBlockFamily::NetworkAuthentication,
confidence: AuthBlockConfidence::Strong,
status: 511,
credential_state: CredentialBlockKind::NotApplicable,
challenge: None,
body_signal: None,
login_redirect: None,
}
}
fn classify_www_authenticate_non_401(
res: &ResponseSurface,
auth_state: RequestAuthState,
) -> Option<AuthBlockSignature> {
let challenge = parse_challenge_header(res, http::header::WWW_AUTHENTICATE)?;
Some(AuthBlockSignature {
family: AuthBlockFamily::OriginAuthentication,
confidence: AuthBlockConfidence::Medium,
status: res.status.as_u16(),
credential_state: credential_state_from_challenge(auth_state, Some(&challenge)),
challenge: Some(challenge),
body_signal: parse_body_signal(res),
login_redirect: None,
})
}
#[allow(clippy::similar_names)] #[must_use]
pub fn classify_403_auth_block(
req: &ProbeDefinition,
res: &ResponseSurface,
) -> Option<AuthBlockSignature> {
let auth_state = RequestAuthState::from_request(req);
let challenge = parse_challenge_header(res, http::header::WWW_AUTHENTICATE);
let body_signal = parse_body_signal(res);
let has_strong_body = body_signal
.as_ref()
.is_some_and(|b| b.confidence == AuthBlockConfidence::Strong);
if challenge.is_none() && !has_strong_body {
return None;
}
let credential_state =
credential_state_403(auth_state, challenge.as_ref(), body_signal.as_ref());
let confidence = strong_if_present(challenge.as_ref());
Some(AuthBlockSignature {
family: AuthBlockFamily::OriginAuthorization,
confidence,
status: 403,
credential_state,
challenge,
body_signal,
login_redirect: None,
})
}
fn classify_login_redirect(res: &ResponseSurface) -> Option<AuthBlockSignature> {
let signal = is_login_redirect(res)?;
Some(AuthBlockSignature {
family: AuthBlockFamily::LoginRedirect,
confidence: signal.confidence,
status: res.status.as_u16(),
credential_state: CredentialBlockKind::NoCredential,
challenge: None,
body_signal: None,
login_redirect: Some(signal),
})
}
fn classify_body_envelope(res: &ResponseSurface) -> Option<AuthBlockSignature> {
let body_signal = parse_body_signal(res)?;
Some(AuthBlockSignature {
family: AuthBlockFamily::AuthErrorEnvelope,
confidence: body_signal.confidence,
status: res.status.as_u16(),
credential_state: CredentialBlockKind::UnknownAuthFailure,
challenge: None,
body_signal: Some(body_signal),
login_redirect: None,
})
}
fn parse_challenge_header(res: &ResponseSurface, name: http::HeaderName) -> Option<AuthChallenge> {
res.headers
.get(name)
.and_then(|v| v.to_str().ok())
.and_then(parse_www_authenticate)
}
fn parse_body_signal(res: &ResponseSurface) -> Option<AuthErrorBodySignal> {
let ct = res
.headers
.get(http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok());
parse_auth_error_body(ct, &res.body)
}
fn strong_if_present<T>(opt: Option<&T>) -> AuthBlockConfidence {
if opt.is_some() {
AuthBlockConfidence::Strong
} else {
AuthBlockConfidence::Medium
}
}
fn credential_state_403(
auth_state: RequestAuthState,
challenge: Option<&AuthChallenge>,
body_signal: Option<&AuthErrorBodySignal>,
) -> CredentialBlockKind {
let challenge_error = challenge.and_then(|c| c.error.as_deref());
let body_code = body_signal.map(|b| b.code.as_str());
let signals_insufficient = matches_token(challenge_error, "insufficient_scope")
|| matches_token(body_code, "insufficient_scope");
let signals_rejected = matches_token(challenge_error, "invalid_token")
|| matches_token(body_code, "invalid_token");
match (auth_state, signals_insufficient, signals_rejected) {
(RequestAuthState::Present, true, _) => CredentialBlockKind::InsufficientScope,
(RequestAuthState::Present, _, true) => CredentialBlockKind::CredentialRejected,
(RequestAuthState::Absent, _, _) if challenge.is_some() => {
CredentialBlockKind::NoCredential
}
_ => CredentialBlockKind::UnknownAuthFailure,
}
}
fn credential_state_from_challenge(
auth_state: RequestAuthState,
challenge: Option<&AuthChallenge>,
) -> CredentialBlockKind {
let error = challenge.and_then(|c| c.error.as_deref());
if matches_token(error, "insufficient_scope") {
return CredentialBlockKind::InsufficientScope;
}
if matches_token(error, "invalid_token") || matches_token(error, "expired_token") {
return CredentialBlockKind::CredentialRejected;
}
match auth_state {
RequestAuthState::Absent => CredentialBlockKind::NoCredential,
RequestAuthState::Present => CredentialBlockKind::UnknownAuthFailure,
}
}
fn matches_token(actual: Option<&str>, expected: &str) -> bool {
actual.is_some_and(|s| s.eq_ignore_ascii_case(expected))
}
#[cfg(test)]
#[path = "auth_classifier_tests.rs"]
mod tests;