firebase_rs_sdk/auth/
error.rs1use crate::app::AppError;
2use crate::auth::types::MultiFactorError;
3use crate::util::FirebaseError;
4use std::borrow::Cow;
5use std::fmt;
6
7pub type AuthResult<T> = Result<T, AuthError>;
8
9#[derive(Debug, Clone)]
10pub enum AuthError {
11 Firebase(FirebaseError),
12 App(AppError),
13 Network(String),
14 InvalidCredential(String),
15 NotImplemented(&'static str),
16 MultiFactorRequired(MultiFactorError),
17 MultiFactor(MultiFactorAuthError),
18}
19
20impl fmt::Display for AuthError {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 match self {
23 AuthError::Firebase(err) => write!(f, "{err}"),
24 AuthError::App(err) => write!(f, "{err}"),
25 AuthError::Network(message) => write!(f, "Network error: {message}"),
26 AuthError::InvalidCredential(message) => write!(f, "Invalid credential: {message}"),
27 AuthError::NotImplemented(feature) => write!(f, "{feature} is not implemented"),
28 AuthError::MultiFactorRequired(err) => write!(f, "{err}"),
29 AuthError::MultiFactor(err) => write!(f, "{err}"),
30 }
31 }
32}
33
34impl std::error::Error for AuthError {}
35
36impl From<FirebaseError> for AuthError {
37 fn from(error: FirebaseError) -> Self {
38 AuthError::Firebase(error)
39 }
40}
41
42impl From<AppError> for AuthError {
43 fn from(error: AppError) -> Self {
44 AuthError::App(error)
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum MultiFactorAuthErrorCode {
54 MissingSession,
56 InvalidSession,
58 MissingInfo,
60 InfoNotFound,
62 ChallengeRequired,
64}
65
66impl MultiFactorAuthErrorCode {
67 fn default_message(self) -> &'static str {
68 match self {
69 MultiFactorAuthErrorCode::MissingSession => {
70 "Multi-factor session is required to continue the challenge"
71 }
72 MultiFactorAuthErrorCode::InvalidSession => {
73 "The supplied multi-factor session is no longer valid"
74 }
75 MultiFactorAuthErrorCode::MissingInfo => {
76 "Required multi-factor enrollment information is missing"
77 }
78 MultiFactorAuthErrorCode::InfoNotFound => {
79 "The requested multi-factor enrollment could not be found"
80 }
81 MultiFactorAuthErrorCode::ChallengeRequired => {
82 "Multi-factor challenge required to complete the operation"
83 }
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct MultiFactorAuthError {
91 code: MultiFactorAuthErrorCode,
92 server_message: Option<String>,
93}
94
95impl MultiFactorAuthError {
96 pub fn new(code: MultiFactorAuthErrorCode, server_message: Option<String>) -> Self {
98 Self {
99 code,
100 server_message,
101 }
102 }
103
104 pub fn code(&self) -> MultiFactorAuthErrorCode {
106 self.code
107 }
108
109 pub fn server_message(&self) -> Option<&str> {
111 self.server_message.as_deref()
112 }
113}
114
115impl fmt::Display for MultiFactorAuthError {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 let base = self.code.default_message();
118 match self.server_message() {
119 Some(detail) if !detail.is_empty() && detail != base => {
120 write!(f, "{base} (server message: {detail})")
121 }
122 _ => write!(f, "{base}"),
123 }
124 }
125}
126
127pub(crate) fn map_mfa_error_code(message: &str) -> Option<AuthError> {
129 let (raw_code, detail) = split_error_message(message);
130 let normalized = normalize_error_code(raw_code);
131
132 let code = match normalized.as_ref() {
133 "INVALID_MFA_SESSION"
134 | "INVALID_MFA_PENDING_CREDENTIAL"
135 | "INVALID_MULTI_FACTOR_SESSION"
136 | "INVALID_MULTI_FACTOR_PENDING_CREDENTIAL" => MultiFactorAuthErrorCode::InvalidSession,
137 "MISSING_MFA_SESSION"
138 | "MISSING_MFA_PENDING_CREDENTIAL"
139 | "MISSING_MULTI_FACTOR_SESSION"
140 | "MISSING_MULTI_FACTOR_PENDING_CREDENTIAL" => MultiFactorAuthErrorCode::MissingSession,
141 "MISSING_MFA_INFO"
142 | "MISSING_MFA_ENROLLMENT_ID"
143 | "MISSING_MULTI_FACTOR_INFO"
144 | "MISSING_MULTI_FACTOR_ENROLLMENT_ID" => MultiFactorAuthErrorCode::MissingInfo,
145 "MFA_INFO_NOT_FOUND"
146 | "MFA_ENROLLMENT_NOT_FOUND"
147 | "MULTI_FACTOR_INFO_NOT_FOUND"
148 | "MULTI_FACTOR_ENROLLMENT_NOT_FOUND" => MultiFactorAuthErrorCode::InfoNotFound,
149 "MFA_REQUIRED" | "MULTI_FACTOR_AUTH_REQUIRED" | "AUTH/MULTI-FACTOR-AUTH-REQUIRED" => {
150 MultiFactorAuthErrorCode::ChallengeRequired
151 }
152 _ => return None,
153 };
154
155 let server_message = detail.map(|value| value.to_string()).or_else(|| {
156 if raw_code.is_empty() {
157 None
158 } else {
159 Some(raw_code.to_string())
160 }
161 });
162 Some(AuthError::MultiFactor(MultiFactorAuthError::new(
163 code,
164 server_message,
165 )))
166}
167
168fn split_error_message(message: &str) -> (&str, Option<&str>) {
169 match message.split_once(':') {
170 Some((code, rest)) => (code.trim(), Some(rest.trim())),
171 None => (message.trim(), None),
172 }
173}
174
175fn normalize_error_code(code: &str) -> Cow<'_, str> {
176 let stripped = code.trim();
177 let without_prefix = stripped.strip_prefix("auth/").unwrap_or(stripped);
178 if without_prefix
179 .chars()
180 .all(|c| c.is_ascii_uppercase() || c == '_')
181 {
182 Cow::Borrowed(without_prefix)
183 } else {
184 let mut candidate = without_prefix
185 .chars()
186 .map(|ch| match ch {
187 '-' => '_',
188 '/' => '_',
189 _ => ch,
190 })
191 .collect::<String>();
192 candidate.make_ascii_uppercase();
193 Cow::Owned(candidate)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn map_mfa_error_code_handles_pending_credential() {
203 let error = map_mfa_error_code("MISSING_MFA_PENDING_CREDENTIAL");
204 match error {
205 Some(AuthError::MultiFactor(err)) => {
206 assert_eq!(err.code(), MultiFactorAuthErrorCode::MissingSession);
207 assert_eq!(err.server_message(), Some("MISSING_MFA_PENDING_CREDENTIAL"));
208 }
209 other => panic!("unexpected mapping result: {other:?}"),
210 }
211 }
212
213 #[test]
214 fn map_mfa_error_code_accepts_auth_prefixed_values() {
215 let error = map_mfa_error_code("auth/multi-factor-info-not-found");
216 match error {
217 Some(AuthError::MultiFactor(err)) => {
218 assert_eq!(err.code(), MultiFactorAuthErrorCode::InfoNotFound);
219 }
220 other => panic!("unexpected mapping result: {other:?}"),
221 }
222 }
223
224 #[test]
225 fn map_mfa_error_code_returns_none_for_unknown_codes() {
226 assert!(map_mfa_error_code("SOME_OTHER_ERROR").is_none());
227 }
228
229 #[test]
230 fn map_mfa_error_code_handles_challenge_required() {
231 let error = map_mfa_error_code("auth/multi-factor-auth-required");
232 match error {
233 Some(AuthError::MultiFactor(err)) => {
234 assert_eq!(err.code(), MultiFactorAuthErrorCode::ChallengeRequired);
235 }
236 other => panic!("unexpected mapping result: {other:?}"),
237 }
238 }
239}