firebase_rs_sdk/auth/
error.rs

1use 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/// Enumerates multi-factor specific error categories surfaced by Firebase Auth.
49///
50/// Mirrors the JavaScript [`AuthErrorCode`](https://github.com/firebase/firebase-js-sdk/blob/HEAD/packages/auth/src/core/errors.ts)
51/// variants related to multi-factor authentication.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum MultiFactorAuthErrorCode {
54    /// The provided multi-factor session (pending credential) is missing from the request.
55    MissingSession,
56    /// The provided multi-factor session (pending credential) is invalid or expired.
57    InvalidSession,
58    /// Required multi-factor enrollment data (e.g. enrollment ID) is missing from the request.
59    MissingInfo,
60    /// The requested multi-factor enrollment could not be found for the current user.
61    InfoNotFound,
62    /// A multi-factor challenge must be completed before the operation can continue.
63    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/// Represents a typed multi-factor error emitted by Firebase Auth REST endpoints.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct MultiFactorAuthError {
91    code: MultiFactorAuthErrorCode,
92    server_message: Option<String>,
93}
94
95impl MultiFactorAuthError {
96    /// Creates a new error with the provided code and optional server-supplied message detail.
97    pub fn new(code: MultiFactorAuthErrorCode, server_message: Option<String>) -> Self {
98        Self {
99            code,
100            server_message,
101        }
102    }
103
104    /// Returns the structured multi-factor error code.
105    pub fn code(&self) -> MultiFactorAuthErrorCode {
106        self.code
107    }
108
109    /// Returns the raw server message if Firebase sent extra context.
110    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
127/// Attempts to convert a REST error message into a typed multi-factor [`AuthError`].
128pub(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}