rhood_core/auth/
auth_state.rs1use secrecy::{ExposeSecret, SecretString};
7
8use crate::ChallengeType;
9
10#[derive(Clone)]
15pub enum AuthState {
16 Unauthenticated,
18 Challenged {
20 challenge_type: ChallengeType,
22 challenge_id: String,
24 },
25 MfaRequired,
27 DeviceVerification {
29 workflow_id: String,
31 },
32 Authenticated {
34 access_token: SecretString,
36 token_type: String,
38 refresh_token: SecretString,
40 },
41}
42
43impl std::fmt::Debug for AuthState {
44 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::Unauthenticated => write!(formatter, "Unauthenticated"),
47 Self::Challenged { challenge_type, .. } => formatter
48 .debug_struct("Challenged")
49 .field("challenge_type", challenge_type)
50 .finish(),
51 Self::MfaRequired => write!(formatter, "MfaRequired"),
52 Self::DeviceVerification { workflow_id } => formatter
53 .debug_struct("DeviceVerification")
54 .field("workflow_id", workflow_id)
55 .finish(),
56 Self::Authenticated { token_type, .. } => formatter
57 .debug_struct("Authenticated")
58 .field("access_token", &"[REDACTED]")
59 .field("token_type", token_type)
60 .field("refresh_token", &"[REDACTED]")
61 .finish(),
62 }
63 }
64}
65
66impl AuthState {
67 pub fn is_authenticated(&self) -> bool {
69 matches!(self, Self::Authenticated { .. })
70 }
71
72 pub fn authorization_header(&self) -> Option<String> {
75 match self {
76 Self::Authenticated {
77 access_token,
78 token_type,
79 ..
80 } => Some(format!("{token_type} {}", access_token.expose_secret())),
81 _ => None,
82 }
83 }
84
85 pub fn refresh_token(&self) -> Option<&SecretString> {
88 match self {
89 Self::Authenticated { refresh_token, .. } => Some(refresh_token),
90 _ => None,
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn unauthenticated_state() {
101 let state = AuthState::Unauthenticated;
102 assert!(!state.is_authenticated());
103 assert!(state.authorization_header().is_none());
104 assert!(state.refresh_token().is_none());
105 }
106
107 #[test]
108 fn authenticated_state() {
109 let state = AuthState::Authenticated {
110 access_token: SecretString::from("tok123"),
111 token_type: "Bearer".into(),
112 refresh_token: SecretString::from("ref456"),
113 };
114 assert!(state.is_authenticated());
115 assert_eq!(state.authorization_header().unwrap(), "Bearer tok123");
116 assert_eq!(state.refresh_token().unwrap().expose_secret(), "ref456");
117 }
118
119 #[test]
120 fn challenged_state() {
121 let state = AuthState::Challenged {
122 challenge_type: ChallengeType::Sms,
123 challenge_id: "abc".into(),
124 };
125 assert!(!state.is_authenticated());
126 assert!(state.authorization_header().is_none());
127 }
128
129 #[test]
130 fn mfa_required_state() {
131 let state = AuthState::MfaRequired;
132 assert!(!state.is_authenticated());
133 }
134
135 #[test]
136 fn device_verification_state() {
137 let state = AuthState::DeviceVerification {
138 workflow_id: "wf-123".into(),
139 };
140 assert!(!state.is_authenticated());
141 let debug = format!("{state:?}");
142 assert!(debug.contains("wf-123"));
143 }
144
145 #[test]
146 fn debug_redacts_tokens() {
147 let state = AuthState::Authenticated {
148 access_token: SecretString::from("super_secret"),
149 token_type: "Bearer".into(),
150 refresh_token: SecretString::from("refresh_secret"),
151 };
152 let debug = format!("{state:?}");
153 assert!(!debug.contains("super_secret"));
154 assert!(!debug.contains("refresh_secret"));
155 assert!(debug.contains("[REDACTED]"));
156 }
157}