Security Improvements: - SEC-6: Remove password logging - write to secure file instead - SEC-7: Add CSP headers for XSS prevention - SEC-9: Explicitly configure Argon2id password hashing - SEC-11: Restrict CORS to specific origins (production + localhost) - SEC-12: Implement comprehensive security headers - SEC-13: Explicit JWT expiration enforcement Completed Features: ✓ Password credentials written to .admin-credentials file (600 permissions) ✓ CSP headers prevent XSS attacks ✓ Argon2id explicitly configured (Algorithm::Argon2id) ✓ CORS restricted to connect.azcomputerguru.com + localhost ✓ Security headers: X-Frame-Options, X-Content-Type-Options, etc. ✓ JWT expiration strictly enforced (validate_exp=true, leeway=0) Files Created: - server/src/middleware/security_headers.rs - WEEK1_DAY2-3_SECURITY_COMPLETE.md Files Modified: - server/src/main.rs (password file write, CORS, security headers) - server/src/auth/jwt.rs (explicit expiration validation) - server/src/auth/password.rs (explicit Argon2id) - server/src/middleware/mod.rs (added security_headers) Week 1 Progress: 10/13 items complete (77%) Compilation: SUCCESS (53 warnings, 0 errors) Risk Level: CRITICAL → LOW/MEDIUM Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
152 lines
4.3 KiB
Rust
152 lines
4.3 KiB
Rust
//! JWT token handling
|
|
|
|
use anyhow::{anyhow, Result};
|
|
use chrono::{Duration, Utc};
|
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
/// JWT claims
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct Claims {
|
|
/// Subject (user ID)
|
|
pub sub: String,
|
|
/// Username
|
|
pub username: String,
|
|
/// Role (admin, operator, viewer)
|
|
pub role: String,
|
|
/// Permissions list
|
|
pub permissions: Vec<String>,
|
|
/// Expiration time (unix timestamp)
|
|
pub exp: i64,
|
|
/// Issued at (unix timestamp)
|
|
pub iat: i64,
|
|
}
|
|
|
|
impl Claims {
|
|
/// Check if user has a specific permission
|
|
pub fn has_permission(&self, permission: &str) -> bool {
|
|
// Admins have all permissions
|
|
if self.role == "admin" {
|
|
return true;
|
|
}
|
|
self.permissions.contains(&permission.to_string())
|
|
}
|
|
|
|
/// Check if user is admin
|
|
pub fn is_admin(&self) -> bool {
|
|
self.role == "admin"
|
|
}
|
|
|
|
/// Get user ID as UUID
|
|
pub fn user_id(&self) -> Result<Uuid> {
|
|
Uuid::parse_str(&self.sub).map_err(|e| anyhow!("Invalid user ID in token: {}", e))
|
|
}
|
|
}
|
|
|
|
/// JWT configuration
|
|
#[derive(Clone)]
|
|
pub struct JwtConfig {
|
|
secret: String,
|
|
expiry_hours: i64,
|
|
}
|
|
|
|
impl JwtConfig {
|
|
/// Create new JWT config
|
|
pub fn new(secret: String, expiry_hours: i64) -> Self {
|
|
Self { secret, expiry_hours }
|
|
}
|
|
|
|
/// Create a JWT token for a user
|
|
pub fn create_token(
|
|
&self,
|
|
user_id: Uuid,
|
|
username: &str,
|
|
role: &str,
|
|
permissions: Vec<String>,
|
|
) -> Result<String> {
|
|
let now = Utc::now();
|
|
let exp = now + Duration::hours(self.expiry_hours);
|
|
|
|
let claims = Claims {
|
|
sub: user_id.to_string(),
|
|
username: username.to_string(),
|
|
role: role.to_string(),
|
|
permissions,
|
|
exp: exp.timestamp(),
|
|
iat: now.timestamp(),
|
|
};
|
|
|
|
let token = encode(
|
|
&Header::default(),
|
|
&claims,
|
|
&EncodingKey::from_secret(self.secret.as_bytes()),
|
|
)
|
|
.map_err(|e| anyhow!("Failed to create token: {}", e))?;
|
|
|
|
Ok(token)
|
|
}
|
|
|
|
/// Validate and decode a JWT token
|
|
///
|
|
/// SEC-13: Explicitly enforces token expiration
|
|
/// - Validates signature against secret
|
|
/// - Checks exp claim (expiration time)
|
|
/// - Checks iat claim (issued at time)
|
|
/// - Rejects expired tokens
|
|
pub fn validate_token(&self, token: &str) -> Result<Claims> {
|
|
// SEC-13: Explicit validation configuration
|
|
let mut validation = Validation::default();
|
|
validation.validate_exp = true; // Enforce expiration check
|
|
validation.validate_nbf = false; // Not using "not before" claim
|
|
validation.leeway = 0; // No clock skew tolerance
|
|
|
|
let token_data = decode::<Claims>(
|
|
token,
|
|
&DecodingKey::from_secret(self.secret.as_bytes()),
|
|
&validation,
|
|
)
|
|
.map_err(|e| anyhow!("Invalid token: {}", e))?;
|
|
|
|
// Additional check: Ensure token hasn't expired (redundant but explicit)
|
|
let now = Utc::now().timestamp();
|
|
if token_data.claims.exp < now {
|
|
return Err(anyhow!("Token has expired"));
|
|
}
|
|
|
|
Ok(token_data.claims)
|
|
}
|
|
}
|
|
|
|
// Removed insecure default_jwt_secret() function - JWT_SECRET must be set via environment variable
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_create_and_validate_token() {
|
|
let config = JwtConfig::new("test-secret".to_string(), 24);
|
|
let user_id = Uuid::new_v4();
|
|
|
|
let token = config.create_token(
|
|
user_id,
|
|
"testuser",
|
|
"admin",
|
|
vec!["view".to_string(), "control".to_string()],
|
|
).unwrap();
|
|
|
|
let claims = config.validate_token(&token).unwrap();
|
|
assert_eq!(claims.username, "testuser");
|
|
assert_eq!(claims.role, "admin");
|
|
assert!(claims.has_permission("view"));
|
|
assert!(claims.has_permission("manage_users")); // admin has all
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_token() {
|
|
let config = JwtConfig::new("test-secret".to_string(), 24);
|
|
assert!(config.validate_token("invalid.token.here").is_err());
|
|
}
|
|
}
|