auth_framework/server/core/
common_validation.rs1use crate::errors::{AuthError, Result};
7use std::collections::HashMap;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10pub mod jwt {
12 use super::*;
13 use jsonwebtoken::decode_header;
14
15 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 decode_header(token)
30 .map_err(|e| AuthError::validation(format!("Invalid JWT header: {}", e)))?;
31
32 Ok(())
33 }
34
35 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 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 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 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 if let Some(iat) = claims.get("iat").and_then(|v| v.as_i64()) {
91 let max_age = 24 * 60 * 60; if now - iat > max_age {
93 return Err(AuthError::validation("Token too old"));
94 }
95 }
96
97 Ok(())
98 }
99
100 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
114pub mod token {
116 use super::*;
117
118 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 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 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 if token.len() < 20 {
147 return Err(AuthError::validation("Refresh token too short"));
148 }
149 Ok(())
150 }
151 _ => Ok(()), }
153 }
154
155 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 for scope in &scopes {
165 if scope.is_empty() {
166 return Err(AuthError::validation("Empty scope value"));
167 }
168
169 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
184pub mod client {
186 use super::*;
187
188 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 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 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 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 if uri.contains('#') {
231 return Err(AuthError::validation(
232 "Redirect URI cannot contain fragments",
233 ));
234 }
235
236 Ok(())
237 }
238
239 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
251pub mod request {
253 use super::*;
254
255 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 pub fn validate_param_format(value: &str, param_name: &str, pattern: &str) -> Result<()> {
273 if value.is_empty() {
275 return Err(AuthError::validation(format!(
276 "Parameter {} cannot be empty",
277 param_name
278 )));
279 }
280
281 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 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 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 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
330pub mod url {
332 use super::*;
333
334 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 if !url.contains("://") {
346 return Err(AuthError::validation("Invalid URL format"));
347 }
348
349 Ok(())
350 }
351
352 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
364pub 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}