Skip to main content

firebase_admin/auth/
error.rs

1//! Error types for the `auth` module.
2
3use crate::core::CoreError;
4use serde::Deserialize;
5
6/// Errors that can occur while verifying an ID token or session cookie.
7#[derive(Debug, thiserror::Error)]
8pub enum TokenVerificationError {
9    /// The token's `exp` claim is in the past.
10    #[error("token has expired")]
11    Expired,
12    /// The token's `iat`/`auth_time` claim is in the future.
13    #[error("token is not yet valid")]
14    NotYetValid,
15    /// The token's signature did not verify against any known public key.
16    #[error("token signature is invalid")]
17    InvalidSignature,
18    /// The token's `aud` claim did not match the configured project ID.
19    #[error("token audience does not match project id")]
20    AudienceMismatch,
21    /// The token's `iss` claim did not match the expected issuer.
22    #[error("token issuer is invalid")]
23    IssuerMismatch,
24    /// The token is missing a `sub` claim, or it is empty.
25    #[error("token is missing a subject claim")]
26    MissingSubject,
27    /// The token could not be decoded or its header/claims could not be parsed.
28    #[error("malformed token: {0}")]
29    Malformed(#[from] jsonwebtoken::errors::Error),
30    /// Google's public keys (JWKS) could not be fetched or parsed.
31    #[error("failed to fetch signing keys: {0}")]
32    Jwks(String),
33}
34
35/// The top-level error type for all `auth` module operations.
36#[derive(Debug, thiserror::Error)]
37pub enum AuthError {
38    /// A lower-level, service-independent error occurred.
39    #[error(transparent)]
40    Core(#[from] CoreError),
41
42    /// The underlying HTTP request failed.
43    #[error("HTTP request failed: {0}")]
44    Http(#[from] reqwest::Error),
45
46    /// ID token or session cookie verification failed.
47    #[error("token verification failed: {0}")]
48    TokenVerification(#[from] TokenVerificationError),
49
50    /// The Firebase Identity Toolkit API returned an error response.
51    #[error("Firebase Auth API error ({status}): {message}")]
52    Api {
53        /// HTTP status code returned by the API.
54        status: u16,
55        /// Human-readable error message returned by the API.
56        message: String,
57        /// Machine-readable error code, when the API provides one.
58        error_code: Option<String>,
59    },
60
61    /// Signing a custom token or session cookie failed.
62    #[error("token signing failed: {0}")]
63    Signing(#[from] jsonwebtoken::errors::Error),
64
65    /// The requested user does not exist.
66    #[error("user not found")]
67    UserNotFound,
68}
69
70/// The `{"error": {...}}` envelope Google APIs use for error responses.
71///
72/// See <https://cloud.google.com/apis/design/errors#http_mapping>. The
73/// Identity Toolkit API additionally nests a well-known short code (e.g.
74/// `EMAIL_EXISTS`, `USER_NOT_FOUND`, `WEAK_PASSWORD`) as the `message` field
75/// of the first entry in `errors`, or as a suffix on the top-level `message`
76/// (`"INVALID_ID_TOKEN : Firebase ID token has ..."`); both shapes appear in
77/// the wild depending on the endpoint, so both are checked.
78#[derive(Debug, Deserialize)]
79struct GoogleApiErrorBody {
80    error: GoogleApiError,
81}
82
83#[derive(Debug, Deserialize)]
84struct GoogleApiError {
85    message: String,
86    #[serde(default)]
87    errors: Vec<GoogleApiErrorDetail>,
88}
89
90#[derive(Debug, Deserialize)]
91struct GoogleApiErrorDetail {
92    reason: Option<String>,
93}
94
95/// Turns a [`reqwest::Response`] into a parsed value, or an
96/// [`AuthError::Api`]/[`AuthError::Core`] if the request failed or the
97/// success body couldn't be deserialized.
98///
99/// Centralizes the "check status, read body, extract Identity Toolkit's
100/// error code" logic shared by every Identity Toolkit call site (session
101/// cookie creation, user management), so error-code parsing only needs to be
102/// correct in one place.
103pub(crate) async fn parse_identity_toolkit_response<T: serde::de::DeserializeOwned>(
104    response: reqwest::Response,
105) -> Result<T, AuthError> {
106    if !response.status().is_success() {
107        let status = response.status().as_u16();
108        let body = response
109            .text()
110            .await
111            .unwrap_or_else(|_| "<no response body>".to_string());
112        return Err(AuthError::from_api_response(status, &body));
113    }
114
115    response
116        .json::<T>()
117        .await
118        .map_err(|e| AuthError::Core(CoreError::Http(e)))
119}
120
121impl AuthError {
122    /// Builds an [`AuthError::Api`] from a non-success HTTP response,
123    /// extracting Identity Toolkit's well-known short error code from the
124    /// response body into `error_code` when present.
125    fn from_api_response(status: u16, body: &str) -> Self {
126        let (message, error_code) = match serde_json::from_str::<GoogleApiErrorBody>(body) {
127            Ok(parsed) => {
128                let code = parsed
129                    .error
130                    .errors
131                    .first()
132                    .and_then(|detail| detail.reason.clone())
133                    .or_else(|| {
134                        // Identity Toolkit often puts the short code as the
135                        // whole message, or as a "CODE : detail" prefix.
136                        parsed
137                            .error
138                            .message
139                            .split(':')
140                            .next()
141                            .map(str::trim)
142                            .filter(|s| {
143                                !s.is_empty()
144                                    && s.chars().all(|c| c.is_ascii_uppercase() || c == '_')
145                            })
146                            .map(str::to_string)
147                    });
148                (parsed.error.message, code)
149            }
150            Err(_) => (body.to_string(), None),
151        };
152
153        AuthError::Api {
154            status,
155            message,
156            error_code,
157        }
158    }
159}