chore: sync repository to current working state
Some checks failed
Build and Test / Build Server (Linux) (push) Has been cancelled
Build and Test / Build Agent (Windows) (push) Has been cancelled
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled

Brings azcomputerguru/guru-connect up to the authoritative working copy that
had been maintained in the claudetools monorepo: Phase 1 security and
infrastructure (middleware, metrics, utils, token blacklist, deployment
scripts, security audits) plus the native-remote-control integration spec.
Preserves the repo .gitignore, .cargo, and server/static/downloads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 06:15:29 -07:00
parent 5b7cf5fb07
commit e3e95f8fa7
73 changed files with 15608 additions and 5757 deletions

View File

@@ -88,26 +88,37 @@ impl JwtConfig {
}
/// 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::default(),
&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)
}
}
/// Default JWT secret if not configured (NOT for production!)
pub fn default_jwt_secret() -> String {
// In production, this should come from environment variable
std::env::var("JWT_SECRET").unwrap_or_else(|_| {
tracing::warn!("JWT_SECRET not set, using default (INSECURE!)");
"guruconnect-dev-secret-change-me-in-production".to_string()
})
}
// Removed insecure default_jwt_secret() function - JWT_SECRET must be set via environment variable
#[cfg(test)]
mod tests {

View File

@@ -5,9 +5,11 @@
pub mod jwt;
pub mod password;
pub mod token_blacklist;
pub use jwt::{Claims, JwtConfig};
pub use password::{hash_password, verify_password, generate_random_password};
pub use token_blacklist::TokenBlacklist;
use axum::{
extract::FromRequestParts,
@@ -98,6 +100,17 @@ where
.get::<Arc<JwtConfig>>()
.ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Auth not configured"))?;
// Get token blacklist from extensions (set by middleware)
let blacklist = parts
.extensions
.get::<Arc<TokenBlacklist>>()
.ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Auth not configured"))?;
// Check if token is revoked
if blacklist.is_revoked(token).await {
return Err((StatusCode::UNAUTHORIZED, "Token has been revoked"));
}
// Validate token
let claims = jwt_config
.validate_token(token)

View File

@@ -1,15 +1,32 @@
//! Password hashing using Argon2id
//!
//! SEC-9: Explicitly uses Argon2id (hybrid variant) for password hashing
//! Argon2id provides resistance against both side-channel and GPU attacks
use anyhow::{anyhow, Result};
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
Argon2, Algorithm, Version, Params,
};
/// Hash a password using Argon2id
///
/// SEC-9: Explicitly configured to use Argon2id variant
/// - Algorithm: Argon2id (hybrid of Argon2i and Argon2d)
/// - Version: 0x13 (latest version)
/// - Memory: 19456 KiB (default)
/// - Iterations: 2 (default)
/// - Parallelism: 1 (default)
pub fn hash_password(password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
// Explicitly use Argon2id (Algorithm::Argon2id)
let argon2 = Argon2::new(
Algorithm::Argon2id, // SEC-9: Explicit Argon2id variant
Version::V0x13, // Latest version
Params::default(), // Default params (19456 KiB, 2 iterations, 1 parallelism)
);
let hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| anyhow!("Failed to hash password: {}", e))?;
@@ -20,6 +37,8 @@ pub fn hash_password(password: &str) -> Result<String> {
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| anyhow!("Invalid password hash format: {}", e))?;
// Argon2::default() uses Argon2id, but we verify against the hash's embedded algorithm
let argon2 = Argon2::default();
Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok())
}

View File

@@ -0,0 +1,164 @@
//! Token blacklist for JWT revocation
//!
//! Provides in-memory token blacklist for immediate revocation of JWTs.
//! Tokens are automatically cleaned up after expiration.
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{info, debug};
/// Token blacklist for revocation
///
/// Maintains a set of revoked token signatures. When a token is revoked
/// (e.g., on logout or admin action), it's added to this blacklist and
/// all subsequent validation attempts will fail.
#[derive(Clone)]
pub struct TokenBlacklist {
/// Set of revoked token strings
tokens: Arc<RwLock<HashSet<String>>>,
}
impl TokenBlacklist {
/// Create a new empty blacklist
pub fn new() -> Self {
Self {
tokens: Arc::new(RwLock::new(HashSet::new())),
}
}
/// Add a token to the blacklist (revoke it)
///
/// # Arguments
/// * `token` - The full JWT token string to revoke
///
/// # Example
/// ```rust
/// blacklist.revoke("eyJ...").await;
/// ```
pub async fn revoke(&self, token: &str) {
let mut tokens = self.tokens.write().await;
let was_new = tokens.insert(token.to_string());
if was_new {
debug!("Token revoked and added to blacklist (length: {})", token.len());
}
}
/// Check if a token has been revoked
///
/// # Arguments
/// * `token` - The JWT token string to check
///
/// # Returns
/// `true` if the token is in the blacklist (revoked), `false` otherwise
pub async fn is_revoked(&self, token: &str) -> bool {
let tokens = self.tokens.read().await;
tokens.contains(token)
}
/// Get the number of tokens currently in the blacklist
pub async fn len(&self) -> usize {
let tokens = self.tokens.read().await;
tokens.len()
}
/// Check if the blacklist is empty
pub async fn is_empty(&self) -> bool {
let tokens = self.tokens.read().await;
tokens.is_empty()
}
/// Remove expired tokens from blacklist (cleanup)
///
/// This should be called periodically to prevent memory buildup.
/// Tokens that can no longer be validated (expired) are removed.
///
/// # Arguments
/// * `jwt_config` - JWT configuration for validating token expiration
///
/// # Returns
/// Number of tokens removed from blacklist
pub async fn cleanup_expired(&self, jwt_config: &super::JwtConfig) -> usize {
let mut tokens = self.tokens.write().await;
let original_len = tokens.len();
// Remove tokens that fail validation (expired)
tokens.retain(|token| {
// If token is expired (validation fails), remove it from blacklist
jwt_config.validate_token(token).is_ok()
});
let removed = original_len - tokens.len();
if removed > 0 {
info!("Cleaned {} expired tokens from blacklist ({} remaining)", removed, tokens.len());
}
removed
}
/// Clear all tokens from the blacklist
///
/// WARNING: This removes all revoked tokens. Use with caution.
pub async fn clear(&self) {
let mut tokens = self.tokens.write().await;
let count = tokens.len();
tokens.clear();
info!("Cleared {} tokens from blacklist", count);
}
}
impl Default for TokenBlacklist {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_revoke_and_check() {
let blacklist = TokenBlacklist::new();
let token = "test.token.here";
assert!(!blacklist.is_revoked(token).await);
blacklist.revoke(token).await;
assert!(blacklist.is_revoked(token).await);
assert_eq!(blacklist.len().await, 1);
}
#[tokio::test]
async fn test_multiple_revocations() {
let blacklist = TokenBlacklist::new();
blacklist.revoke("token1").await;
blacklist.revoke("token2").await;
blacklist.revoke("token3").await;
assert_eq!(blacklist.len().await, 3);
assert!(blacklist.is_revoked("token1").await);
assert!(blacklist.is_revoked("token2").await);
assert!(blacklist.is_revoked("token3").await);
assert!(!blacklist.is_revoked("token4").await);
}
#[tokio::test]
async fn test_clear() {
let blacklist = TokenBlacklist::new();
blacklist.revoke("token1").await;
blacklist.revoke("token2").await;
assert_eq!(blacklist.len().await, 2);
blacklist.clear().await;
assert_eq!(blacklist.len().await, 0);
assert!(blacklist.is_empty().await);
}
}