Skip to main content

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