Skip to main content

supabase_client_auth/
error.rs

1use serde::Deserialize;
2use std::fmt;
3use supabase_client_core::SupabaseError;
4
5/// Error response format from the GoTrue API.
6///
7/// GoTrue may return errors in different shapes; this covers the common fields.
8#[derive(Debug, Clone, Deserialize)]
9pub struct GoTrueErrorResponse {
10    #[serde(default)]
11    pub error: Option<String>,
12    #[serde(default)]
13    pub error_description: Option<String>,
14    #[serde(default)]
15    pub msg: Option<String>,
16    #[serde(default)]
17    pub message: Option<String>,
18    #[serde(default)]
19    pub code: Option<i32>,
20    #[serde(default)]
21    pub error_code: Option<String>,
22}
23
24impl GoTrueErrorResponse {
25    /// Extract the most informative error message from the response.
26    pub fn error_message(&self) -> String {
27        self.msg
28            .as_deref()
29            .or(self.message.as_deref())
30            .or(self.error_description.as_deref())
31            .or(self.error.as_deref())
32            .unwrap_or("Unknown error")
33            .to_string()
34    }
35}
36
37/// Auth-specific errors.
38#[derive(Debug, thiserror::Error)]
39pub enum AuthError {
40    /// HTTP transport error from reqwest.
41    #[error("HTTP error: {0}")]
42    Http(#[from] reqwest::Error),
43
44    /// GoTrue API returned an error response.
45    #[error("Auth API error ({status}): {message}")]
46    Api {
47        status: u16,
48        message: String,
49        #[source]
50        error_code: Option<AuthErrorCode>,
51    },
52
53    /// Invalid configuration (missing URL or key).
54    #[error("Invalid auth configuration: {0}")]
55    InvalidConfig(String),
56
57    /// Session has expired.
58    #[error("Session expired")]
59    SessionExpired,
60
61    /// No active session.
62    #[error("No active session")]
63    NoSession,
64
65    /// JSON serialization/deserialization error.
66    #[error("Serialization error: {0}")]
67    Serialization(#[from] serde_json::Error),
68
69    /// URL parsing error.
70    #[error("URL parse error: {0}")]
71    UrlParse(#[from] url::ParseError),
72
73    /// Invalid or malformed JWT token.
74    #[error("Invalid token: {0}")]
75    InvalidToken(String),
76}
77
78/// Known GoTrue error codes for programmatic matching.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum AuthErrorCode {
81    InvalidCredentials,
82    UserNotFound,
83    UserAlreadyExists,
84    EmailNotConfirmed,
85    PhoneNotConfirmed,
86    SessionNotFound,
87    RefreshTokenNotFound,
88    OtpExpired,
89    OtpDisabled,
90    WeakPassword,
91    SamePassword,
92    ValidationFailed,
93    OverRequestRateLimit,
94    // MFA error codes
95    MfaFactorNameConflict,
96    MfaFactorNotFound,
97    MfaChallengeExpired,
98    MfaVerificationFailed,
99    MfaVerificationRejected,
100    MfaIpAddressMismatch,
101    MfaEnrollNotEnabled,
102    MfaVerifyNotEnabled,
103    // SSO error codes
104    SsoProviderNotFound,
105    SsoDomainAlreadyExists,
106    // Identity error codes
107    IdentityAlreadyExists,
108    IdentityNotFound,
109    ManualLinkingDisabled,
110    SingleIdentityNotDeletable,
111    // OAuth server error codes
112    OAuthClientNotFound,
113    OAuthClientAlreadyExists,
114    OAuthInvalidGrant,
115    Unknown(String),
116}
117
118impl fmt::Display for AuthErrorCode {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            Self::InvalidCredentials => write!(f, "invalid_credentials"),
122            Self::UserNotFound => write!(f, "user_not_found"),
123            Self::UserAlreadyExists => write!(f, "user_already_exists"),
124            Self::EmailNotConfirmed => write!(f, "email_not_confirmed"),
125            Self::PhoneNotConfirmed => write!(f, "phone_not_confirmed"),
126            Self::SessionNotFound => write!(f, "session_not_found"),
127            Self::RefreshTokenNotFound => write!(f, "refresh_token_not_found"),
128            Self::OtpExpired => write!(f, "otp_expired"),
129            Self::OtpDisabled => write!(f, "otp_disabled"),
130            Self::WeakPassword => write!(f, "weak_password"),
131            Self::SamePassword => write!(f, "same_password"),
132            Self::ValidationFailed => write!(f, "validation_failed"),
133            Self::OverRequestRateLimit => write!(f, "over_request_rate_limit"),
134            Self::MfaFactorNameConflict => write!(f, "mfa_factor_name_conflict"),
135            Self::MfaFactorNotFound => write!(f, "mfa_factor_not_found"),
136            Self::MfaChallengeExpired => write!(f, "mfa_challenge_expired"),
137            Self::MfaVerificationFailed => write!(f, "mfa_verification_failed"),
138            Self::MfaVerificationRejected => write!(f, "mfa_verification_rejected"),
139            Self::MfaIpAddressMismatch => write!(f, "mfa_ip_address_mismatch"),
140            Self::MfaEnrollNotEnabled => write!(f, "mfa_enroll_not_enabled"),
141            Self::MfaVerifyNotEnabled => write!(f, "mfa_verify_not_enabled"),
142            Self::SsoProviderNotFound => write!(f, "sso_provider_not_found"),
143            Self::SsoDomainAlreadyExists => write!(f, "sso_domain_already_exists"),
144            Self::IdentityAlreadyExists => write!(f, "identity_already_exists"),
145            Self::IdentityNotFound => write!(f, "identity_not_found"),
146            Self::ManualLinkingDisabled => write!(f, "manual_linking_disabled"),
147            Self::SingleIdentityNotDeletable => write!(f, "single_identity_not_deletable"),
148            Self::OAuthClientNotFound => write!(f, "oauth_client_not_found"),
149            Self::OAuthClientAlreadyExists => write!(f, "oauth_client_already_exists"),
150            Self::OAuthInvalidGrant => write!(f, "oauth_invalid_grant"),
151            Self::Unknown(code) => write!(f, "{}", code),
152        }
153    }
154}
155
156impl std::error::Error for AuthErrorCode {}
157
158impl From<&str> for AuthErrorCode {
159    fn from(s: &str) -> Self {
160        match s {
161            "invalid_credentials" => Self::InvalidCredentials,
162            "user_not_found" => Self::UserNotFound,
163            "user_already_exists" => Self::UserAlreadyExists,
164            "email_not_confirmed" => Self::EmailNotConfirmed,
165            "phone_not_confirmed" => Self::PhoneNotConfirmed,
166            "session_not_found" => Self::SessionNotFound,
167            "refresh_token_not_found" => Self::RefreshTokenNotFound,
168            "otp_expired" => Self::OtpExpired,
169            "otp_disabled" => Self::OtpDisabled,
170            "weak_password" => Self::WeakPassword,
171            "same_password" => Self::SamePassword,
172            "validation_failed" => Self::ValidationFailed,
173            "over_request_rate_limit" => Self::OverRequestRateLimit,
174            "mfa_factor_name_conflict" => Self::MfaFactorNameConflict,
175            "mfa_factor_not_found" => Self::MfaFactorNotFound,
176            "mfa_challenge_expired" => Self::MfaChallengeExpired,
177            "mfa_verification_failed" => Self::MfaVerificationFailed,
178            "mfa_verification_rejected" => Self::MfaVerificationRejected,
179            "mfa_ip_address_mismatch" => Self::MfaIpAddressMismatch,
180            "mfa_enroll_not_enabled" => Self::MfaEnrollNotEnabled,
181            "mfa_verify_not_enabled" => Self::MfaVerifyNotEnabled,
182            "sso_provider_not_found" => Self::SsoProviderNotFound,
183            "sso_domain_already_exists" => Self::SsoDomainAlreadyExists,
184            "identity_already_exists" => Self::IdentityAlreadyExists,
185            "identity_not_found" => Self::IdentityNotFound,
186            "manual_linking_disabled" => Self::ManualLinkingDisabled,
187            "single_identity_not_deletable" => Self::SingleIdentityNotDeletable,
188            "oauth_client_not_found" => Self::OAuthClientNotFound,
189            "oauth_client_already_exists" => Self::OAuthClientAlreadyExists,
190            "oauth_invalid_grant" => Self::OAuthInvalidGrant,
191            other => Self::Unknown(other.to_string()),
192        }
193    }
194}
195
196impl From<AuthError> for SupabaseError {
197    fn from(err: AuthError) -> Self {
198        SupabaseError::Auth(err.to_string())
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn mfa_error_codes_roundtrip() {
208        let codes = [
209            ("mfa_factor_name_conflict", AuthErrorCode::MfaFactorNameConflict),
210            ("mfa_factor_not_found", AuthErrorCode::MfaFactorNotFound),
211            ("mfa_challenge_expired", AuthErrorCode::MfaChallengeExpired),
212            ("mfa_verification_failed", AuthErrorCode::MfaVerificationFailed),
213            ("mfa_verification_rejected", AuthErrorCode::MfaVerificationRejected),
214            ("mfa_ip_address_mismatch", AuthErrorCode::MfaIpAddressMismatch),
215            ("mfa_enroll_not_enabled", AuthErrorCode::MfaEnrollNotEnabled),
216            ("mfa_verify_not_enabled", AuthErrorCode::MfaVerifyNotEnabled),
217        ];
218        for (s, expected) in &codes {
219            let parsed: AuthErrorCode = (*s).into();
220            assert_eq!(parsed, *expected);
221            assert_eq!(parsed.to_string(), *s);
222        }
223    }
224
225    #[test]
226    fn sso_error_codes_roundtrip() {
227        let parsed: AuthErrorCode = "sso_provider_not_found".into();
228        assert_eq!(parsed, AuthErrorCode::SsoProviderNotFound);
229        assert_eq!(parsed.to_string(), "sso_provider_not_found");
230
231        let parsed: AuthErrorCode = "sso_domain_already_exists".into();
232        assert_eq!(parsed, AuthErrorCode::SsoDomainAlreadyExists);
233    }
234
235    #[test]
236    fn identity_error_codes_roundtrip() {
237        let codes = [
238            ("identity_already_exists", AuthErrorCode::IdentityAlreadyExists),
239            ("identity_not_found", AuthErrorCode::IdentityNotFound),
240            ("manual_linking_disabled", AuthErrorCode::ManualLinkingDisabled),
241            ("single_identity_not_deletable", AuthErrorCode::SingleIdentityNotDeletable),
242        ];
243        for (s, expected) in &codes {
244            let parsed: AuthErrorCode = (*s).into();
245            assert_eq!(parsed, *expected);
246            assert_eq!(parsed.to_string(), *s);
247        }
248    }
249
250    #[test]
251    fn oauth_error_codes_roundtrip() {
252        let codes = [
253            ("oauth_client_not_found", AuthErrorCode::OAuthClientNotFound),
254            ("oauth_client_already_exists", AuthErrorCode::OAuthClientAlreadyExists),
255            ("oauth_invalid_grant", AuthErrorCode::OAuthInvalidGrant),
256        ];
257        for (s, expected) in &codes {
258            let parsed: AuthErrorCode = (*s).into();
259            assert_eq!(parsed, *expected);
260            assert_eq!(parsed.to_string(), *s);
261        }
262    }
263}