auth_framework/server/core/
common_validation.rs

1//! Common Validation Utilities
2//!
3//! This module provides shared validation functions to eliminate
4//! duplication across server modules.
5
6use crate::errors::{AuthError, Result};
7use std::collections::HashMap;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10/// Common JWT validation utilities
11pub mod jwt {
12    use super::*;
13    use jsonwebtoken::decode_header;
14
15    /// Validate JWT structure and format
16    pub fn validate_jwt_format(token: &str) -> Result<()> {
17        if token.is_empty() {
18            return Err(AuthError::validation("JWT token is empty"));
19        }
20
21        let parts: Vec<&str> = token.split('.').collect();
22        if parts.len() != 3 {
23            return Err(AuthError::validation(
24                "Invalid JWT format: must have 3 parts",
25            ));
26        }
27
28        // Validate header can be decoded
29        decode_header(token)
30            .map_err(|e| AuthError::validation(format!("Invalid JWT header: {}", e)))?;
31
32        Ok(())
33    }
34
35    /// Extract claims without signature validation (for inspection ONLY)
36    ///
37    /// # Security Warning
38    /// This function does NOT validate the JWT signature, making it vulnerable to:
39    /// - Token forgery
40    /// - Data tampering
41    /// - Man-in-the-middle attacks
42    ///
43    /// Only use for:
44    /// - Token inspection/debugging
45    /// - Extracting metadata before validation
46    /// - Non-security-critical operations
47    ///
48    /// Never use for authentication or authorization decisions!
49    pub fn extract_claims_unsafe(token: &str) -> Result<serde_json::Value> {
50        validate_jwt_format(token)?;
51
52        let parts: Vec<&str> = token.split('.').collect();
53        let payload = parts[1];
54
55        use base64::Engine as _;
56        use base64::engine::general_purpose::URL_SAFE_NO_PAD;
57
58        let decoded = URL_SAFE_NO_PAD
59            .decode(payload)
60            .map_err(|e| AuthError::validation(format!("Invalid JWT payload encoding: {}", e)))?;
61
62        let claims: serde_json::Value = serde_json::from_slice(&decoded)
63            .map_err(|e| AuthError::validation(format!("Invalid JWT payload JSON: {}", e)))?;
64
65        Ok(claims)
66    }
67
68    /// Validate JWT timestamp claims (exp, iat, nbf)
69    pub fn validate_time_claims(claims: &serde_json::Value) -> Result<()> {
70        let now = SystemTime::now()
71            .duration_since(UNIX_EPOCH)
72            .unwrap()
73            .as_secs() as i64;
74
75        // Check expiration
76        if let Some(exp) = claims.get("exp").and_then(|v| v.as_i64())
77            && now >= exp
78        {
79            return Err(AuthError::validation("Token has expired"));
80        }
81
82        // Check not before
83        if let Some(nbf) = claims.get("nbf").and_then(|v| v.as_i64())
84            && now < nbf
85        {
86            return Err(AuthError::validation("Token not yet valid (nbf)"));
87        }
88
89        // Check issued at (reasonable bounds)
90        if let Some(iat) = claims.get("iat").and_then(|v| v.as_i64()) {
91            let max_age = 24 * 60 * 60; // 24 hours
92            if now - iat > max_age {
93                return Err(AuthError::validation("Token too old"));
94            }
95        }
96
97        Ok(())
98    }
99
100    /// Validate required JWT claims
101    pub fn validate_required_claims(claims: &serde_json::Value, required: &[&str]) -> Result<()> {
102        for claim in required {
103            if claims.get(claim).is_none() {
104                return Err(AuthError::validation(format!(
105                    "Missing required claim: {}",
106                    claim
107                )));
108            }
109        }
110        Ok(())
111    }
112}
113
114/// Common token validation utilities
115pub mod token {
116    use super::*;
117
118    /// Token type validation
119    pub fn validate_token_type(token_type: &str, allowed_types: &[&str]) -> Result<()> {
120        if !allowed_types.contains(&token_type) {
121            return Err(AuthError::validation(format!(
122                "Unsupported token type: {}",
123                token_type
124            )));
125        }
126        Ok(())
127    }
128
129    /// Validate token format (basic structure)
130    pub fn validate_token_format(token: &str, token_type: &str) -> Result<()> {
131        if token.is_empty() {
132            return Err(AuthError::validation("Token is empty"));
133        }
134
135        match token_type {
136            "urn:ietf:params:oauth:token-type:jwt" => jwt::validate_jwt_format(token),
137            "urn:ietf:params:oauth:token-type:access_token" => {
138                // Bearer token validation
139                if token.len() < 10 {
140                    return Err(AuthError::validation("Access token too short"));
141                }
142                Ok(())
143            }
144            "urn:ietf:params:oauth:token-type:refresh_token" => {
145                // Refresh token validation
146                if token.len() < 20 {
147                    return Err(AuthError::validation("Refresh token too short"));
148                }
149                Ok(())
150            }
151            _ => Ok(()), // Allow other token types
152        }
153    }
154
155    /// Validate scope format
156    pub fn validate_scope(scope: &str) -> Result<Vec<String>> {
157        if scope.is_empty() {
158            return Ok(vec![]);
159        }
160
161        let scopes: Vec<String> = scope.split_whitespace().map(|s| s.to_string()).collect();
162
163        // Validate each scope
164        for scope in &scopes {
165            if scope.is_empty() {
166                return Err(AuthError::validation("Empty scope value"));
167            }
168
169            // Basic scope format validation
170            if !scope.chars().all(|c| {
171                c.is_alphanumeric() || c == ':' || c == '/' || c == '.' || c == '-' || c == '_'
172            }) {
173                return Err(AuthError::validation(format!(
174                    "Invalid scope format: {}",
175                    scope
176                )));
177            }
178        }
179
180        Ok(scopes)
181    }
182}
183
184/// Common client validation utilities
185pub mod client {
186    use super::*;
187
188    /// Validate client ID format
189    pub fn validate_client_id(client_id: &str) -> Result<()> {
190        if client_id.is_empty() {
191            return Err(AuthError::validation("Client ID is empty"));
192        }
193
194        if client_id.len() < 3 {
195            return Err(AuthError::validation("Client ID too short"));
196        }
197
198        if client_id.len() > 255 {
199            return Err(AuthError::validation("Client ID too long"));
200        }
201
202        // Validate character set
203        if !client_id
204            .chars()
205            .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.')
206        {
207            return Err(AuthError::validation(
208                "Client ID contains invalid characters",
209            ));
210        }
211
212        Ok(())
213    }
214
215    /// Validate redirect URI
216    pub fn validate_redirect_uri(uri: &str) -> Result<()> {
217        if uri.is_empty() {
218            return Err(AuthError::validation("Redirect URI is empty"));
219        }
220
221        // Must be absolute URI
222        if !uri.starts_with("http://")
223            && !uri.starts_with("https://")
224            && !uri.starts_with("custom://")
225        {
226            return Err(AuthError::validation("Redirect URI must be absolute"));
227        }
228
229        // No fragments allowed
230        if uri.contains('#') {
231            return Err(AuthError::validation(
232                "Redirect URI cannot contain fragments",
233            ));
234        }
235
236        Ok(())
237    }
238
239    /// Validate grant type
240    pub fn validate_grant_type(grant_type: &str, allowed_grants: &[&str]) -> Result<()> {
241        if !allowed_grants.contains(&grant_type) {
242            return Err(AuthError::validation(format!(
243                "Unsupported grant type: {}",
244                grant_type
245            )));
246        }
247        Ok(())
248    }
249}
250
251/// Common request validation utilities
252pub mod request {
253    use super::*;
254
255    /// Validate required parameters
256    pub fn validate_required_params(
257        params: &HashMap<String, String>,
258        required: &[&str],
259    ) -> Result<()> {
260        for param in required {
261            if !params.contains_key(*param) || params[*param].trim().is_empty() {
262                return Err(AuthError::validation(format!(
263                    "Missing parameter: {}",
264                    param
265                )));
266            }
267        }
268        Ok(())
269    }
270
271    /// Validate parameter format
272    pub fn validate_param_format(value: &str, param_name: &str, pattern: &str) -> Result<()> {
273        // Basic validation without regex for now
274        if value.is_empty() {
275            return Err(AuthError::validation(format!(
276                "Parameter {} cannot be empty",
277                param_name
278            )));
279        }
280
281        // Basic pattern checks
282        match pattern {
283            "alphanum" => {
284                if !value.chars().all(|c| c.is_alphanumeric()) {
285                    return Err(AuthError::validation(format!(
286                        "Parameter {} must be alphanumeric",
287                        param_name
288                    )));
289                }
290            }
291            _ => {
292                // For now, just check it's not empty
293                if value.trim().is_empty() {
294                    return Err(AuthError::validation(format!(
295                        "Parameter {} has invalid format",
296                        param_name
297                    )));
298                }
299            }
300        }
301
302        Ok(())
303    }
304
305    /// Validate code challenge method
306    pub fn validate_code_challenge_method(method: &str) -> Result<()> {
307        match method {
308            "plain" | "S256" => Ok(()),
309            _ => Err(AuthError::validation("Invalid code challenge method")),
310        }
311    }
312
313    /// Validate response type
314    pub fn validate_response_type(response_type: &str, allowed_types: &[&str]) -> Result<()> {
315        let types: Vec<&str> = response_type.split_whitespace().collect();
316
317        for response_type in &types {
318            if !allowed_types.contains(response_type) {
319                return Err(AuthError::validation(format!(
320                    "Unsupported response type: {}",
321                    response_type
322                )));
323            }
324        }
325
326        Ok(())
327    }
328}
329
330/// Common URL validation utilities
331pub mod url {
332    use super::*;
333
334    /// Validate URL format and accessibility
335    pub fn validate_url_format(url: &str) -> Result<()> {
336        if url.is_empty() {
337            return Err(AuthError::validation("URL is empty"));
338        }
339
340        if !url.starts_with("http://") && !url.starts_with("https://") {
341            return Err(AuthError::validation("URL must use HTTP or HTTPS scheme"));
342        }
343
344        // Basic URL parsing validation - simplified without url crate for now
345        if !url.contains("://") {
346            return Err(AuthError::validation("Invalid URL format"));
347        }
348
349        Ok(())
350    }
351
352    /// Validate HTTPS requirement
353    pub fn validate_https_required(url: &str) -> Result<()> {
354        validate_url_format(url)?;
355
356        if !url.starts_with("https://") {
357            return Err(AuthError::validation("HTTPS is required"));
358        }
359
360        Ok(())
361    }
362}
363
364/// Collects and aggregates validation errors from multiple validation operations.
365///
366/// This function takes a vector of validation results and combines any errors
367/// into a single error message. If all validations pass, returns Ok(()).
368/// If any validations fail, returns an error containing all error messages.
369///
370/// # Arguments
371///
372/// * `validations` - A vector of validation results to aggregate
373///
374/// # Returns
375///
376/// * `Ok(())` if all validations passed
377/// * `Err(AuthError)` containing aggregated error messages if any validations failed
378///
379/// # Example
380///
381/// ```rust,no_run
382/// use auth_framework::server::core::common_validation::collect_validation_errors;
383/// use auth_framework::errors::Result;
384///
385/// # fn validate_client_id(_client_id: &str) -> Result<()> { Ok(()) }
386/// # fn validate_scope(_scope: &str) -> Result<()> { Ok(()) }
387/// # fn validate_redirect_uri(_uri: &str) -> Result<()> { Ok(()) }
388/// let validations = vec![
389///     validate_client_id("valid_client"),
390///     validate_scope("read write"),
391///     validate_redirect_uri("https://example.com/callback"),
392/// ];
393///
394/// let result = collect_validation_errors(validations);
395/// match result {
396///     Ok(()) => println!("All validations passed"),
397///     Err(e) => println!("Validation errors: {}", e),
398/// }
399/// ```
400pub fn collect_validation_errors(validations: Vec<Result<()>>) -> Result<()> {
401    let errors: Vec<String> = validations
402        .into_iter()
403        .filter_map(|result| result.err())
404        .map(|e| format!("{}", e))
405        .collect();
406
407    if errors.is_empty() {
408        Ok(())
409    } else {
410        Err(AuthError::validation(errors.join("; ")))
411    }
412}