use crate::authn::factor::FactorKind;
use crate::authn::ids::DeviceId;
use std::sync::Arc;
#[derive(Debug)]
pub enum LoginOutcome {
FactorRequired(FactorKind),
Locked {
until: Option<chrono::DateTime<chrono::Utc>>,
},
InvalidCredentials,
StepUpRequired {
device_id: DeviceId,
allowed_factors: Vec<FactorKind>,
},
}
pub enum PrepareOutcome {
Ready,
SendOtp {
code: crate::authn::factor::ZeroizedString,
destination: Arc<str>,
},
AlreadySent {
destination: Arc<str>,
},
Fido2Challenge {
challenge: serde_json::Value,
},
}
impl std::fmt::Debug for PrepareOutcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ready => write!(f, "Ready"),
Self::SendOtp { destination, .. } => f
.debug_struct("SendOtp")
.field("code", &"***")
.field("destination", destination)
.finish(),
Self::AlreadySent { destination } => f
.debug_struct("AlreadySent")
.field("destination", destination)
.finish(),
Self::Fido2Challenge { .. } => f.debug_struct("Fido2Challenge").finish_non_exhaustive(),
}
}
}
#[cfg(test)]
mod outcomes_tests {
use super::*;
#[test]
fn login_outcome_step_up_required_carries_device_id_and_factors() {
use crate::authn::ids::testing as id_fixtures;
let device_id = id_fixtures::device("d-step-up");
let outcome = LoginOutcome::StepUpRequired {
device_id,
allowed_factors: vec![FactorKind::Totp],
};
match outcome {
LoginOutcome::StepUpRequired {
device_id: got_id,
allowed_factors,
} => {
assert_eq!(got_id, device_id);
assert_eq!(allowed_factors, vec![FactorKind::Totp]);
}
other => panic!("expected StepUpRequired, got {other:?}"),
}
}
#[test]
fn prepare_outcome_debug_redacts_code_and_includes_variant_and_destination() {
let ready = format!("{:?}", PrepareOutcome::Ready);
assert_eq!(ready, "Ready", "Ready variant must format as the bare name");
let send_otp = format!(
"{:?}",
PrepareOutcome::SendOtp {
code: crate::authn::factor::ZeroizedString::new("12345678"),
destination: Arc::from("user@example.com"),
}
);
assert!(
send_otp.contains("SendOtp"),
"SendOtp variant name must appear"
);
assert!(
send_otp.contains("user@example.com"),
"destination must appear (got {send_otp:?})"
);
assert!(
!send_otp.contains("12345678"),
"plaintext code must NOT appear in Debug output (got {send_otp:?})"
);
assert!(
send_otp.contains("***"),
"redacted placeholder must appear (got {send_otp:?})"
);
let already_sent = format!(
"{:?}",
PrepareOutcome::AlreadySent {
destination: Arc::from("alt@example.com"),
}
);
assert!(already_sent.contains("AlreadySent"));
assert!(already_sent.contains("alt@example.com"));
let fido2 = format!(
"{:?}",
PrepareOutcome::Fido2Challenge {
challenge: serde_json::json!({"opaque": true}),
}
);
assert!(fido2.contains("Fido2Challenge"));
assert!(
!fido2.contains("opaque"),
"Debug surface must use finish_non_exhaustive to avoid leaking challenge contents (got {fido2:?})"
);
}
}
#[derive(Debug)]
pub enum FactorOutcome {
Authenticated,
FactorRequired(FactorKind),
InvalidCredential,
Locked {
until: Option<chrono::DateTime<chrono::Utc>>,
},
}
#[derive(Debug)]
pub enum SignupOutcome {
Started,
AlreadyExists,
TenantNotActive,
}