Skip to main content

fraiseql_auth/
error_sanitizer.rs

1//! Error sanitization layer — separates user-facing messages from internal details.
2//!
3//! Authentication errors often contain internal information that must never reach API
4//! clients (database query details, key material references, stack traces). This module
5//! provides [`SanitizedError`] and [`AuthErrorSanitizer`] to ensure only safe, generic
6//! messages are returned in API responses while full details are retained for server-side
7//! logging.
8
9use std::fmt;
10
11/// A sanitizable error that separates user-facing and internal messages
12#[derive(Debug, Clone)]
13pub struct SanitizedError {
14    /// User-facing message (safe for API responses)
15    user_message:     String,
16    /// Internal message (for logging only)
17    internal_message: String,
18}
19
20impl SanitizedError {
21    /// Create a new sanitized error
22    pub fn new(user_message: impl Into<String>, internal_message: impl Into<String>) -> Self {
23        Self {
24            user_message:     user_message.into(),
25            internal_message: internal_message.into(),
26        }
27    }
28
29    /// Get the user-facing message (safe for API responses)
30    #[must_use]
31    pub fn user_facing(&self) -> &str {
32        &self.user_message
33    }
34
35    /// Get the internal message (for logging only)
36    #[must_use]
37    pub fn internal(&self) -> &str {
38        &self.internal_message
39    }
40}
41
42impl fmt::Display for SanitizedError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        // Display uses user-facing message (safe for logs)
45        write!(f, "{}", self.user_message)
46    }
47}
48
49impl std::error::Error for SanitizedError {}
50
51/// Helper trait for creating sanitized errors from standard error types.
52///
53/// The method `.sanitized(msg)` converts any `Display` type into a [`SanitizedError`],
54/// keeping the original message for server-side logging while returning a safe message
55/// in the API response.
56pub trait Sanitize {
57    /// Convert to a sanitized error
58    fn sanitized(self, user_message: &str) -> SanitizedError;
59}
60
61impl<E: fmt::Display> Sanitize for E {
62    fn sanitized(self, user_message: &str) -> SanitizedError {
63        SanitizedError::new(user_message, self.to_string())
64    }
65}
66
67/// Pre-defined error messages for common authentication scenarios
68pub mod messages {
69    /// Generic authentication failure message
70    pub const AUTH_FAILED: &str = "Authentication failed";
71
72    /// Generic permission denied message
73    pub const PERMISSION_DENIED: &str = "Permission denied";
74
75    /// Generic service error message
76    pub const SERVICE_UNAVAILABLE: &str = "Service temporarily unavailable";
77
78    /// Generic request error message
79    pub const REQUEST_FAILED: &str = "Request failed";
80
81    /// Invalid state (CSRF token)
82    pub const INVALID_STATE: &str = "Authentication failed";
83
84    /// Token expired
85    pub const TOKEN_EXPIRED: &str = "Authentication failed";
86
87    /// Invalid signature
88    pub const INVALID_SIGNATURE: &str = "Authentication failed";
89
90    /// Session expired
91    pub const SESSION_EXPIRED: &str = "Authentication failed";
92
93    /// Session revoked
94    pub const SESSION_REVOKED: &str = "Authentication failed";
95}
96
97/// Error sanitization for authentication errors
98pub struct AuthErrorSanitizer;
99
100impl AuthErrorSanitizer {
101    /// Sanitize JWT validation error
102    #[must_use]
103    pub fn jwt_validation_error(internal_error: &str) -> SanitizedError {
104        SanitizedError::new(messages::AUTH_FAILED, internal_error)
105    }
106
107    /// Sanitize OIDC provider error
108    #[must_use]
109    pub fn oidc_provider_error(internal_error: &str) -> SanitizedError {
110        SanitizedError::new(messages::AUTH_FAILED, internal_error)
111    }
112
113    /// Sanitize session token error
114    #[must_use]
115    pub fn session_token_error(internal_error: &str) -> SanitizedError {
116        SanitizedError::new(messages::AUTH_FAILED, internal_error)
117    }
118
119    /// Sanitize CSRF state error
120    #[must_use]
121    pub fn csrf_state_error(internal_error: &str) -> SanitizedError {
122        SanitizedError::new(messages::INVALID_STATE, internal_error)
123    }
124
125    /// Sanitize permission/authorization error
126    #[must_use]
127    pub fn permission_error(internal_error: &str) -> SanitizedError {
128        SanitizedError::new(messages::PERMISSION_DENIED, internal_error)
129    }
130
131    /// Sanitize database error
132    #[must_use]
133    pub fn database_error(internal_error: &str) -> SanitizedError {
134        SanitizedError::new(messages::SERVICE_UNAVAILABLE, internal_error)
135    }
136}