Skip to main content

arete_auth/
error.rs

1use thiserror::Error;
2
3/// Machine-readable error codes for authentication failures
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum AuthErrorCode {
6    /// Missing authentication token
7    TokenMissing,
8    /// Token has expired
9    TokenExpired,
10    /// Invalid token signature
11    TokenInvalidSignature,
12    /// Invalid token format
13    TokenInvalidFormat,
14    /// Token issuer mismatch
15    TokenInvalidIssuer,
16    /// Token audience mismatch
17    TokenInvalidAudience,
18    /// Required claim missing from token
19    TokenMissingClaim,
20    /// Token key ID not found
21    TokenKeyNotFound,
22    /// Origin mismatch for token
23    OriginMismatch,
24    /// Origin is required but not provided
25    OriginRequired,
26    /// Rate limit exceeded (token minting)
27    RateLimitExceeded,
28    /// Connection limit exceeded for subject
29    ConnectionLimitExceeded,
30    /// Subscription limit exceeded
31    SubscriptionLimitExceeded,
32    /// Snapshot limit exceeded
33    SnapshotLimitExceeded,
34    /// Egress limit exceeded
35    EgressLimitExceeded,
36    /// Invalid static token
37    InvalidStaticToken,
38    /// Internal server error during auth
39    InternalError,
40}
41
42impl AuthErrorCode {
43    /// Returns the error code as a kebab-case string identifier
44    pub fn as_str(&self) -> &'static str {
45        match self {
46            AuthErrorCode::TokenMissing => "token-missing",
47            AuthErrorCode::TokenExpired => "token-expired",
48            AuthErrorCode::TokenInvalidSignature => "token-invalid-signature",
49            AuthErrorCode::TokenInvalidFormat => "token-invalid-format",
50            AuthErrorCode::TokenInvalidIssuer => "token-invalid-issuer",
51            AuthErrorCode::TokenInvalidAudience => "token-invalid-audience",
52            AuthErrorCode::TokenMissingClaim => "token-missing-claim",
53            AuthErrorCode::TokenKeyNotFound => "token-key-not-found",
54            AuthErrorCode::OriginMismatch => "origin-mismatch",
55            AuthErrorCode::OriginRequired => "origin-required",
56            AuthErrorCode::RateLimitExceeded => "rate-limit-exceeded",
57            AuthErrorCode::ConnectionLimitExceeded => "connection-limit-exceeded",
58            AuthErrorCode::SubscriptionLimitExceeded => "subscription-limit-exceeded",
59            AuthErrorCode::SnapshotLimitExceeded => "snapshot-limit-exceeded",
60            AuthErrorCode::EgressLimitExceeded => "egress-limit-exceeded",
61            AuthErrorCode::InvalidStaticToken => "invalid-static-token",
62            AuthErrorCode::InternalError => "internal-error",
63        }
64    }
65
66    /// Returns whether the client should retry with the same token
67    pub fn should_retry(&self) -> bool {
68        matches!(
69            self,
70            AuthErrorCode::RateLimitExceeded | AuthErrorCode::InternalError
71        )
72    }
73
74    /// Returns whether the client should fetch a new token
75    pub fn should_refresh_token(&self) -> bool {
76        matches!(
77            self,
78            AuthErrorCode::TokenExpired
79                | AuthErrorCode::TokenInvalidSignature
80                | AuthErrorCode::TokenInvalidFormat
81                | AuthErrorCode::TokenInvalidIssuer
82                | AuthErrorCode::TokenInvalidAudience
83                | AuthErrorCode::TokenKeyNotFound
84        )
85    }
86
87    /// Returns the HTTP status code equivalent for this error
88    pub fn http_status(&self) -> u16 {
89        match self {
90            AuthErrorCode::TokenMissing => 401,
91            AuthErrorCode::TokenExpired => 401,
92            AuthErrorCode::TokenInvalidSignature => 401,
93            AuthErrorCode::TokenInvalidFormat => 400,
94            AuthErrorCode::TokenInvalidIssuer => 401,
95            AuthErrorCode::TokenInvalidAudience => 401,
96            AuthErrorCode::TokenMissingClaim => 400,
97            AuthErrorCode::TokenKeyNotFound => 401,
98            AuthErrorCode::OriginMismatch => 403,
99            AuthErrorCode::OriginRequired => 403,
100            AuthErrorCode::RateLimitExceeded => 429,
101            AuthErrorCode::ConnectionLimitExceeded => 429,
102            AuthErrorCode::SubscriptionLimitExceeded => 429,
103            AuthErrorCode::SnapshotLimitExceeded => 429,
104            AuthErrorCode::EgressLimitExceeded => 429,
105            AuthErrorCode::InvalidStaticToken => 401,
106            AuthErrorCode::InternalError => 500,
107        }
108    }
109
110    /// Returns the default retry policy for this error
111    pub fn default_retry_policy(&self) -> RetryPolicy {
112        use std::time::Duration;
113
114        match self {
115            // Token errors - refresh token and retry
116            AuthErrorCode::TokenExpired
117            | AuthErrorCode::TokenInvalidSignature
118            | AuthErrorCode::TokenInvalidFormat
119            | AuthErrorCode::TokenInvalidIssuer
120            | AuthErrorCode::TokenInvalidAudience
121            | AuthErrorCode::TokenKeyNotFound => RetryPolicy::RetryWithFreshToken,
122
123            // Rate limits - retry after delay
124            AuthErrorCode::RateLimitExceeded
125            | AuthErrorCode::ConnectionLimitExceeded
126            | AuthErrorCode::SubscriptionLimitExceeded
127            | AuthErrorCode::SnapshotLimitExceeded
128            | AuthErrorCode::EgressLimitExceeded => RetryPolicy::RetryWithBackoff {
129                initial: Duration::from_secs(1),
130                max: Duration::from_secs(60),
131            },
132
133            // Internal errors - retry with backoff
134            AuthErrorCode::InternalError => RetryPolicy::RetryWithBackoff {
135                initial: Duration::from_secs(1),
136                max: Duration::from_secs(30),
137            },
138
139            // Everything else - don't retry
140            _ => RetryPolicy::NoRetry,
141        }
142    }
143}
144
145/// Retry policy for authentication errors
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum RetryPolicy {
148    /// Do not retry this request
149    NoRetry,
150    /// Retry immediately (for transient errors)
151    RetryImmediately,
152    /// Retry after a specific duration
153    RetryAfter(std::time::Duration),
154    /// Retry with exponential backoff
155    RetryWithBackoff {
156        /// Initial backoff duration
157        initial: std::time::Duration,
158        /// Maximum backoff duration
159        max: std::time::Duration,
160    },
161    /// Refresh the token before retrying
162    RetryWithFreshToken,
163}
164
165impl std::fmt::Display for AuthErrorCode {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        write!(f, "{}", self.as_str())
168    }
169}
170
171/// Convert VerifyError to AuthErrorCode
172impl From<&VerifyError> for AuthErrorCode {
173    fn from(err: &VerifyError) -> Self {
174        match err {
175            VerifyError::Expired => AuthErrorCode::TokenExpired,
176            VerifyError::NotYetValid => AuthErrorCode::TokenInvalidFormat,
177            VerifyError::InvalidSignature => AuthErrorCode::TokenInvalidSignature,
178            VerifyError::InvalidIssuer => AuthErrorCode::TokenInvalidIssuer,
179            VerifyError::InvalidAudience => AuthErrorCode::TokenInvalidAudience,
180            VerifyError::MissingClaim(_) => AuthErrorCode::TokenMissingClaim,
181            VerifyError::OriginMismatch { .. } => AuthErrorCode::OriginMismatch,
182            VerifyError::OriginRequired { .. } => AuthErrorCode::OriginRequired,
183            VerifyError::DecodeError(_) => AuthErrorCode::TokenInvalidFormat,
184            VerifyError::KeyNotFound(_) => AuthErrorCode::TokenKeyNotFound,
185            VerifyError::InvalidFormat(_) => AuthErrorCode::TokenInvalidFormat,
186            VerifyError::Revoked => AuthErrorCode::TokenExpired,
187            VerifyError::InvalidPolicyClaims(_) => AuthErrorCode::TokenInvalidFormat,
188        }
189    }
190}
191
192/// Authentication errors
193#[derive(Debug, Error)]
194pub enum AuthError {
195    #[error("invalid key format: {0}")]
196    InvalidKeyFormat(String),
197
198    #[error("key loading failed: {0}")]
199    KeyLoadingFailed(String),
200
201    #[error("signing failed: {0}")]
202    SigningFailed(String),
203
204    #[error("IO error: {0}")]
205    Io(#[from] std::io::Error),
206}
207
208/// Token verification errors
209#[derive(Debug, Error, Clone, PartialEq)]
210pub enum VerifyError {
211    #[error("token has expired")]
212    Expired,
213
214    #[error("token is not yet valid")]
215    NotYetValid,
216
217    #[error("invalid signature")]
218    InvalidSignature,
219
220    #[error("invalid issuer")]
221    InvalidIssuer,
222
223    #[error("invalid audience")]
224    InvalidAudience,
225
226    #[error("missing required claim: {0}")]
227    MissingClaim(String),
228
229    #[error("origin mismatch: expected {expected}, got {actual}")]
230    OriginMismatch { expected: String, actual: String },
231
232    #[error("origin header required but not provided (token is origin-bound to '{token_origin}')")]
233    OriginRequired { token_origin: String },
234
235    #[error("decode error: {0}")]
236    DecodeError(String),
237
238    #[error("key not found: {0}")]
239    KeyNotFound(String),
240
241    #[error("invalid token format: {0}")]
242    InvalidFormat(String),
243
244    #[error("token has been revoked")]
245    Revoked,
246
247    #[error("invalid policy claims: {0}")]
248    InvalidPolicyClaims(String),
249}