use secrecy::{ExposeSecret, SecretString};
use crate::ChallengeType;
#[derive(Clone)]
pub enum AuthState {
Unauthenticated,
Challenged {
challenge_type: ChallengeType,
challenge_id: String,
},
MfaRequired,
DeviceVerification {
workflow_id: String,
},
Authenticated {
access_token: SecretString,
token_type: String,
refresh_token: SecretString,
},
}
impl std::fmt::Debug for AuthState {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unauthenticated => write!(formatter, "Unauthenticated"),
Self::Challenged { challenge_type, .. } => formatter
.debug_struct("Challenged")
.field("challenge_type", challenge_type)
.finish(),
Self::MfaRequired => write!(formatter, "MfaRequired"),
Self::DeviceVerification { workflow_id } => formatter
.debug_struct("DeviceVerification")
.field("workflow_id", workflow_id)
.finish(),
Self::Authenticated { token_type, .. } => formatter
.debug_struct("Authenticated")
.field("access_token", &"[REDACTED]")
.field("token_type", token_type)
.field("refresh_token", &"[REDACTED]")
.finish(),
}
}
}
impl AuthState {
pub fn is_authenticated(&self) -> bool {
matches!(self, Self::Authenticated { .. })
}
pub fn authorization_header(&self) -> Option<String> {
match self {
Self::Authenticated {
access_token,
token_type,
..
} => Some(format!("{token_type} {}", access_token.expose_secret())),
_ => None,
}
}
pub fn refresh_token(&self) -> Option<&SecretString> {
match self {
Self::Authenticated { refresh_token, .. } => Some(refresh_token),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unauthenticated_state() {
let state = AuthState::Unauthenticated;
assert!(!state.is_authenticated());
assert!(state.authorization_header().is_none());
assert!(state.refresh_token().is_none());
}
#[test]
fn authenticated_state() {
let state = AuthState::Authenticated {
access_token: SecretString::from("tok123"),
token_type: "Bearer".into(),
refresh_token: SecretString::from("ref456"),
};
assert!(state.is_authenticated());
assert_eq!(state.authorization_header().unwrap(), "Bearer tok123");
assert_eq!(state.refresh_token().unwrap().expose_secret(), "ref456");
}
#[test]
fn challenged_state() {
let state = AuthState::Challenged {
challenge_type: ChallengeType::Sms,
challenge_id: "abc".into(),
};
assert!(!state.is_authenticated());
assert!(state.authorization_header().is_none());
}
#[test]
fn mfa_required_state() {
let state = AuthState::MfaRequired;
assert!(!state.is_authenticated());
}
#[test]
fn device_verification_state() {
let state = AuthState::DeviceVerification {
workflow_id: "wf-123".into(),
};
assert!(!state.is_authenticated());
let debug = format!("{state:?}");
assert!(debug.contains("wf-123"));
}
#[test]
fn debug_redacts_tokens() {
let state = AuthState::Authenticated {
access_token: SecretString::from("super_secret"),
token_type: "Bearer".into(),
refresh_token: SecretString::from("refresh_secret"),
};
let debug = format!("{state:?}");
assert!(!debug.contains("super_secret"));
assert!(!debug.contains("refresh_secret"));
assert!(debug.contains("[REDACTED]"));
}
}