use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AuthErrorCode {
TokenMissing,
TokenExpired,
TokenInvalidSignature,
TokenInvalidFormat,
TokenInvalidIssuer,
TokenInvalidAudience,
TokenMissingClaim,
TokenKeyNotFound,
OriginMismatch,
OriginRequired,
RateLimitExceeded,
ConnectionLimitExceeded,
SubscriptionLimitExceeded,
SnapshotLimitExceeded,
EgressLimitExceeded,
InvalidStaticToken,
InternalError,
}
impl AuthErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
AuthErrorCode::TokenMissing => "token-missing",
AuthErrorCode::TokenExpired => "token-expired",
AuthErrorCode::TokenInvalidSignature => "token-invalid-signature",
AuthErrorCode::TokenInvalidFormat => "token-invalid-format",
AuthErrorCode::TokenInvalidIssuer => "token-invalid-issuer",
AuthErrorCode::TokenInvalidAudience => "token-invalid-audience",
AuthErrorCode::TokenMissingClaim => "token-missing-claim",
AuthErrorCode::TokenKeyNotFound => "token-key-not-found",
AuthErrorCode::OriginMismatch => "origin-mismatch",
AuthErrorCode::OriginRequired => "origin-required",
AuthErrorCode::RateLimitExceeded => "rate-limit-exceeded",
AuthErrorCode::ConnectionLimitExceeded => "connection-limit-exceeded",
AuthErrorCode::SubscriptionLimitExceeded => "subscription-limit-exceeded",
AuthErrorCode::SnapshotLimitExceeded => "snapshot-limit-exceeded",
AuthErrorCode::EgressLimitExceeded => "egress-limit-exceeded",
AuthErrorCode::InvalidStaticToken => "invalid-static-token",
AuthErrorCode::InternalError => "internal-error",
}
}
pub fn should_retry(&self) -> bool {
matches!(
self,
AuthErrorCode::RateLimitExceeded | AuthErrorCode::InternalError
)
}
pub fn should_refresh_token(&self) -> bool {
matches!(
self,
AuthErrorCode::TokenExpired
| AuthErrorCode::TokenInvalidSignature
| AuthErrorCode::TokenInvalidFormat
| AuthErrorCode::TokenInvalidIssuer
| AuthErrorCode::TokenInvalidAudience
| AuthErrorCode::TokenKeyNotFound
)
}
pub fn http_status(&self) -> u16 {
match self {
AuthErrorCode::TokenMissing => 401,
AuthErrorCode::TokenExpired => 401,
AuthErrorCode::TokenInvalidSignature => 401,
AuthErrorCode::TokenInvalidFormat => 400,
AuthErrorCode::TokenInvalidIssuer => 401,
AuthErrorCode::TokenInvalidAudience => 401,
AuthErrorCode::TokenMissingClaim => 400,
AuthErrorCode::TokenKeyNotFound => 401,
AuthErrorCode::OriginMismatch => 403,
AuthErrorCode::OriginRequired => 403,
AuthErrorCode::RateLimitExceeded => 429,
AuthErrorCode::ConnectionLimitExceeded => 429,
AuthErrorCode::SubscriptionLimitExceeded => 429,
AuthErrorCode::SnapshotLimitExceeded => 429,
AuthErrorCode::EgressLimitExceeded => 429,
AuthErrorCode::InvalidStaticToken => 401,
AuthErrorCode::InternalError => 500,
}
}
pub fn default_retry_policy(&self) -> RetryPolicy {
use std::time::Duration;
match self {
AuthErrorCode::TokenExpired
| AuthErrorCode::TokenInvalidSignature
| AuthErrorCode::TokenInvalidFormat
| AuthErrorCode::TokenInvalidIssuer
| AuthErrorCode::TokenInvalidAudience
| AuthErrorCode::TokenKeyNotFound => RetryPolicy::RetryWithFreshToken,
AuthErrorCode::RateLimitExceeded
| AuthErrorCode::ConnectionLimitExceeded
| AuthErrorCode::SubscriptionLimitExceeded
| AuthErrorCode::SnapshotLimitExceeded
| AuthErrorCode::EgressLimitExceeded => RetryPolicy::RetryWithBackoff {
initial: Duration::from_secs(1),
max: Duration::from_secs(60),
},
AuthErrorCode::InternalError => RetryPolicy::RetryWithBackoff {
initial: Duration::from_secs(1),
max: Duration::from_secs(30),
},
_ => RetryPolicy::NoRetry,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryPolicy {
NoRetry,
RetryImmediately,
RetryAfter(std::time::Duration),
RetryWithBackoff {
initial: std::time::Duration,
max: std::time::Duration,
},
RetryWithFreshToken,
}
impl std::fmt::Display for AuthErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl From<&VerifyError> for AuthErrorCode {
fn from(err: &VerifyError) -> Self {
match err {
VerifyError::Expired => AuthErrorCode::TokenExpired,
VerifyError::NotYetValid => AuthErrorCode::TokenInvalidFormat,
VerifyError::InvalidSignature => AuthErrorCode::TokenInvalidSignature,
VerifyError::InvalidIssuer => AuthErrorCode::TokenInvalidIssuer,
VerifyError::InvalidAudience => AuthErrorCode::TokenInvalidAudience,
VerifyError::MissingClaim(_) => AuthErrorCode::TokenMissingClaim,
VerifyError::OriginMismatch { .. } => AuthErrorCode::OriginMismatch,
VerifyError::OriginRequired { .. } => AuthErrorCode::OriginRequired,
VerifyError::DecodeError(_) => AuthErrorCode::TokenInvalidFormat,
VerifyError::KeyNotFound(_) => AuthErrorCode::TokenKeyNotFound,
VerifyError::InvalidFormat(_) => AuthErrorCode::TokenInvalidFormat,
VerifyError::Revoked => AuthErrorCode::TokenExpired,
}
}
}
#[derive(Debug, Error)]
pub enum AuthError {
#[error("invalid key format: {0}")]
InvalidKeyFormat(String),
#[error("key loading failed: {0}")]
KeyLoadingFailed(String),
#[error("signing failed: {0}")]
SigningFailed(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Error, Clone, PartialEq)]
pub enum VerifyError {
#[error("token has expired")]
Expired,
#[error("token is not yet valid")]
NotYetValid,
#[error("invalid signature")]
InvalidSignature,
#[error("invalid issuer")]
InvalidIssuer,
#[error("invalid audience")]
InvalidAudience,
#[error("missing required claim: {0}")]
MissingClaim(String),
#[error("origin mismatch: expected {expected}, got {actual}")]
OriginMismatch { expected: String, actual: String },
#[error("origin header required but not provided (token is origin-bound to '{token_origin}')")]
OriginRequired { token_origin: String },
#[error("decode error: {0}")]
DecodeError(String),
#[error("key not found: {0}")]
KeyNotFound(String),
#[error("invalid token format: {0}")]
InvalidFormat(String),
#[error("token has been revoked")]
Revoked,
}