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            VerifyError::SealedClaims(_) => AuthErrorCode::TokenInvalidFormat,
189        }
190    }
191}
192
193/// Authentication errors
194#[derive(Debug, Error)]
195pub enum AuthError {
196    #[error("invalid key format: {0}")]
197    InvalidKeyFormat(String),
198
199    #[error("key loading failed: {0}")]
200    KeyLoadingFailed(String),
201
202    #[error("signing failed: {0}")]
203    SigningFailed(String),
204
205    #[error("IO error: {0}")]
206    Io(#[from] std::io::Error),
207}
208
209/// Token verification errors
210#[derive(Debug, Error, Clone, PartialEq)]
211pub enum VerifyError {
212    #[error("token has expired")]
213    Expired,
214
215    #[error("token is not yet valid")]
216    NotYetValid,
217
218    #[error("invalid signature")]
219    InvalidSignature,
220
221    #[error("invalid issuer")]
222    InvalidIssuer,
223
224    #[error("invalid audience")]
225    InvalidAudience,
226
227    #[error("missing required claim: {0}")]
228    MissingClaim(String),
229
230    #[error("origin mismatch: expected {expected}, got {actual}")]
231    OriginMismatch { expected: String, actual: String },
232
233    #[error("origin header required but not provided (token is origin-bound to '{token_origin}')")]
234    OriginRequired { token_origin: String },
235
236    #[error("decode error: {0}")]
237    DecodeError(String),
238
239    #[error("key not found: {0}")]
240    KeyNotFound(String),
241
242    #[error("invalid token format: {0}")]
243    InvalidFormat(String),
244
245    #[error("token has been revoked")]
246    Revoked,
247
248    #[error("invalid policy claims: {0}")]
249    InvalidPolicyClaims(String),
250
251    #[error("sealed claims: {0}")]
252    SealedClaims(#[from] crate::sealed::SealError),
253}