Files
claudetools/projects/msp-tools/guru-connect/server/src/auth/jwt.rs
Mike Swanson e3176a18c6 Week 1 Day 2-3: Complete remaining security fixes (SEC-6 through SEC-13)
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>
2026-01-17 19:35:59 -07:00

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());
}
}