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}