use crate::core::CoreError;
use serde::Deserialize;
#[derive(Debug, thiserror::Error)]
pub enum TokenVerificationError {
#[error("token has expired")]
Expired,
#[error("token is not yet valid")]
NotYetValid,
#[error("token signature is invalid")]
InvalidSignature,
#[error("token audience does not match project id")]
AudienceMismatch,
#[error("token issuer is invalid")]
IssuerMismatch,
#[error("token is missing a subject claim")]
MissingSubject,
#[error("malformed token: {0}")]
Malformed(#[from] jsonwebtoken::errors::Error),
#[error("failed to fetch signing keys: {0}")]
Jwks(String),
}
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error(transparent)]
Core(#[from] CoreError),
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("token verification failed: {0}")]
TokenVerification(#[from] TokenVerificationError),
#[error("Firebase Auth API error ({status}): {message}")]
Api {
status: u16,
message: String,
error_code: Option<String>,
},
#[error("token signing failed: {0}")]
Signing(#[from] jsonwebtoken::errors::Error),
#[error("user not found")]
UserNotFound,
}
#[derive(Debug, Deserialize)]
struct GoogleApiErrorBody {
error: GoogleApiError,
}
#[derive(Debug, Deserialize)]
struct GoogleApiError {
message: String,
#[serde(default)]
errors: Vec<GoogleApiErrorDetail>,
}
#[derive(Debug, Deserialize)]
struct GoogleApiErrorDetail {
reason: Option<String>,
}
pub(crate) async fn parse_identity_toolkit_response<T: serde::de::DeserializeOwned>(
response: reqwest::Response,
) -> Result<T, AuthError> {
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response
.text()
.await
.unwrap_or_else(|_| "<no response body>".to_string());
return Err(AuthError::from_api_response(status, &body));
}
response
.json::<T>()
.await
.map_err(|e| AuthError::Core(CoreError::Http(e)))
}
impl AuthError {
fn from_api_response(status: u16, body: &str) -> Self {
let (message, error_code) = match serde_json::from_str::<GoogleApiErrorBody>(body) {
Ok(parsed) => {
let code = parsed
.error
.errors
.first()
.and_then(|detail| detail.reason.clone())
.or_else(|| {
parsed
.error
.message
.split(':')
.next()
.map(str::trim)
.filter(|s| {
!s.is_empty()
&& s.chars().all(|c| c.is_ascii_uppercase() || c == '_')
})
.map(str::to_string)
});
(parsed.error.message, code)
}
Err(_) => (body.to_string(), None),
};
AuthError::Api {
status,
message,
error_code,
}
}
}