5 Commits
Author SHA1 Message Date
thakares 579f415a9f tests: random string to bypass the restricted sequence validator fixes
Rust CI / Rust CI (stable) (push) Canceled after 0s
2026-08-07 20:09:31 +05:30
thakares 9c410cc717 tests: random string to bypass the restricted sequence validator fixes 2026-08-07 20:08:21 +05:30
thakares 312c78dbbb style: apply rustfmt after clippy let-chain fixes 2026-08-07 19:53:33 +05:30
thakares b25a015898 refactor: apply clippy let-chains and harden password validation
- Auto-fix 29 clippy warnings by converting nested if-lets to let-chains
- Harden password strength validator to reject restricted substrings
- Simplify rate_limiter state checks using is_none_or
- Improves idiomatic Rust style and overall security posture
2026-08-07 19:48:25 +05:30
thakares f2f615b456 chore(release): bump version to v0.4.0 2026-08-07 19:07:20 +05:30
21 changed files with 229 additions and 216 deletions

No files matched your search

Generated
+1 -1
View File
@@ -1239,7 +1239,7 @@ dependencies = [
[[package]] [[package]]
name = "nx9-auth" name = "nx9-auth"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nx9-auth" name = "nx9-auth"
version = "0.3.0" version = "0.4.0"
edition = "2024" edition = "2024"
authors = ["NX9 Team","Sunil Thakare"] authors = ["NX9 Team","Sunil Thakare"]
description = "Lightweight self-hosted IAM service for the NX9 ecosystem" description = "Lightweight self-hosted IAM service for the NX9 ecosystem"
+1 -1
View File
@@ -6,7 +6,7 @@
*Self-Hosted • Privacy-First • Pure Rust • Single Binary • SQLite & PostgreSQL* *Self-Hosted • Privacy-First • Pure Rust • Single Binary • SQLite & PostgreSQL*
[![Version](https://img.shields.io/badge/version-v0.3.0-blue.svg)]() [![Version](https://img.shields.io/badge/version-v0.4.0-blue.svg)]()
[![Rust](https://img.shields.io/badge/Rust-2024-orange.svg)](https://www.rust-lang.org/) [![Rust](https://img.shields.io/badge/Rust-2024-orange.svg)](https://www.rust-lang.org/)
[![License](https://img.shields.io/badge/license-Apache2--0%20%7C%20MIT-green.svg)](LICENSE) [![License](https://img.shields.io/badge/license-Apache2--0%20%7C%20MIT-green.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Linux-success.svg)]() [![Platform](https://img.shields.io/badge/platform-Linux-success.svg)]()
+12 -12
View File
@@ -68,10 +68,10 @@ pub async fn login(
} }
// Rate limit check (per IP) // Rate limit check (per IP)
if let Some(ip_str) = &ctx.ip_address { if let Some(ip_str) = &ctx.ip_address
if let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>() { && let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>()
state.rate_limiter.check(ip_addr)?; {
} state.rate_limiter.check(ip_addr)?;
} }
// Look up user — always run comparable work on failure paths (timing). // Look up user — always run comparable work on failure paths (timing).
@@ -103,10 +103,10 @@ pub async fn login(
if !is_authed { if !is_authed {
record_login_failure(&state, body.username.trim(), ip, ctx.user_agent.as_deref()).await; record_login_failure(&state, body.username.trim(), ip, ctx.user_agent.as_deref()).await;
if let Some(ip_str) = &ctx.ip_address { if let Some(ip_str) = &ctx.ip_address
if let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>() { && let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>()
state.rate_limiter.record_failure(ip_addr); {
} state.rate_limiter.record_failure(ip_addr);
} }
// Non-enumerating error for both unknown user and bad password. // Non-enumerating error for both unknown user and bad password.
return Err(AppError::InvalidCredentials); return Err(AppError::InvalidCredentials);
@@ -118,10 +118,10 @@ pub async fn login(
}; };
// Clear rate limit on success // Clear rate limit on success
if let Some(ip_str) = &ctx.ip_address { if let Some(ip_str) = &ctx.ip_address
if let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>() { && let Ok(ip_addr) = ip_str.parse::<std::net::IpAddr>()
state.rate_limiter.record_success(ip_addr); {
} state.rate_limiter.record_success(ip_addr);
} }
// Session fixation mitigation: revoke prior sessions + refresh tokens. // Session fixation mitigation: revoke prior sessions + refresh tokens.
+6 -6
View File
@@ -103,12 +103,12 @@ pub async fn terminate_session(
Path(id): Path<String>, Path(id): Path<String>,
) -> Result<Json<Value>> { ) -> Result<Json<Value>> {
// If the user is trying to terminate the current session, disallow it // If the user is trying to terminate the current session, disallow it
if let Some(current_id) = auth.session_id.as_deref() { if let Some(current_id) = auth.session_id.as_deref()
if id == current_id { && id == current_id
return Err(AppError::InvalidInput( {
"Cannot terminate current session".into(), return Err(AppError::InvalidInput(
)); "Cannot terminate current session".into(),
} ));
} }
// Admins can terminate any session, users can only terminate their own // Admins can terminate any session, users can only terminate their own
+8 -8
View File
@@ -53,10 +53,10 @@ pub async fn create_tenant(
) -> Result<Json<Value>> { ) -> Result<Json<Value>> {
require(&state.provider, &auth.user.id, "roles:manage").await?; require(&state.provider, &auth.user.id, "roles:manage").await?;
if let Some(ref s) = body.slug { if let Some(ref s) = body.slug
if !s.trim().is_empty() { && !s.trim().is_empty()
crate::identity::slug::validate_slug(s)?; {
} crate::identity::slug::validate_slug(s)?;
} }
let id = uuid::Uuid::new_v4().to_string(); let id = uuid::Uuid::new_v4().to_string();
@@ -124,10 +124,10 @@ pub async fn update_tenant(
) -> Result<Json<Value>> { ) -> Result<Json<Value>> {
require(&state.provider, &auth.user.id, "roles:manage").await?; require(&state.provider, &auth.user.id, "roles:manage").await?;
if let Some(ref s) = body.slug { if let Some(ref s) = body.slug
if !s.trim().is_empty() { && !s.trim().is_empty()
crate::identity::slug::validate_slug(s)?; {
} crate::identity::slug::validate_slug(s)?;
} }
state state
+7 -7
View File
@@ -26,13 +26,13 @@ pub fn ui_dist_dir() -> PathBuf {
return c.clone(); return c.clone();
} }
} }
if let Ok(exe) = std::env::current_exe() { if let Ok(exe) = std::env::current_exe()
if let Some(dir) = exe.parent() { && let Some(dir) = exe.parent()
for rel in ["ui/dist", "../ui/dist", "../../ui/dist"] { {
let candidate = dir.join(rel); for rel in ["ui/dist", "../ui/dist", "../../ui/dist"] {
if candidate.exists() { let candidate = dir.join(rel);
return candidate; if candidate.exists() {
} return candidate;
} }
} }
} }
+17 -16
View File
@@ -566,10 +566,10 @@ async fn cmd_init(
let sqlite_path = config.database.sqlite_path(); let sqlite_path = config.database.sqlite_path();
let db_path = std::path::Path::new(&sqlite_path); let db_path = std::path::Path::new(&sqlite_path);
println!("Creating database directory..."); println!("Creating database directory...");
if let Some(parent) = db_path.parent() { if let Some(parent) = db_path.parent()
if !parent.as_os_str().is_empty() { && !parent.as_os_str().is_empty()
std::fs::create_dir_all(parent)?; {
} std::fs::create_dir_all(parent)?;
} }
// Create state directory // Create state directory
@@ -664,10 +664,11 @@ async fn run_init_validation(config: &Config, admin_skipped: bool) -> anyhow::Re
let sqlite_path = config.database.sqlite_path(); let sqlite_path = config.database.sqlite_path();
let db_path = std::path::Path::new(&sqlite_path); let db_path = std::path::Path::new(&sqlite_path);
let mut dirs_ok = true; let mut dirs_ok = true;
if let Some(parent) = db_path.parent() { if let Some(parent) = db_path.parent()
if !parent.as_os_str().is_empty() && std::fs::create_dir_all(parent).is_err() { && !parent.as_os_str().is_empty()
dirs_ok = false; && std::fs::create_dir_all(parent).is_err()
} {
dirs_ok = false;
} }
if let Ok(home) = std::env::var("HOME") { if let Ok(home) = std::env::var("HOME") {
let state_dir = std::path::Path::new(&home).join(".local/state/nx9-auth"); let state_dir = std::path::Path::new(&home).join(".local/state/nx9-auth");
@@ -892,10 +893,10 @@ async fn cmd_backup(config: &Config, path: &std::path::Path) -> anyhow::Result<(
); );
} }
if let Some(parent) = path.parent() { if let Some(parent) = path.parent()
if !parent.as_os_str().is_empty() { && !parent.as_os_str().is_empty()
std::fs::create_dir_all(parent)?; {
} std::fs::create_dir_all(parent)?;
} }
if path.exists() { if path.exists() {
@@ -958,10 +959,10 @@ async fn cmd_restore(config: &Config, path: &std::path::Path) -> anyhow::Result<
crate::config::DatabaseBackend::Sqlite => { crate::config::DatabaseBackend::Sqlite => {
let sqlite_path = config.database.sqlite_path(); let sqlite_path = config.database.sqlite_path();
let target_path = std::path::Path::new(&sqlite_path); let target_path = std::path::Path::new(&sqlite_path);
if let Some(parent) = target_path.parent() { if let Some(parent) = target_path.parent()
if !parent.as_os_str().is_empty() { && !parent.as_os_str().is_empty()
std::fs::create_dir_all(parent)?; {
} std::fs::create_dir_all(parent)?;
} }
std::fs::copy(path, target_path).with_context(|| { std::fs::copy(path, target_path).with_context(|| {
format!("failed to restore backup to {}", target_path.display()) format!("failed to restore backup to {}", target_path.display())
+16 -16
View File
@@ -293,13 +293,13 @@ impl Default for ShutdownConfig {
// ── Helpers ────────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────────
fn resolve_home_path(path: &str) -> String { fn resolve_home_path(path: &str) -> String {
if let Some(stripped) = path.strip_prefix("~/") { if let Some(stripped) = path.strip_prefix("~/")
if let Ok(home) = std::env::var("HOME") { && let Ok(home) = std::env::var("HOME")
return Path::new(&home) {
.join(stripped) return Path::new(&home)
.to_string_lossy() .join(stripped)
.into_owned(); .to_string_lossy()
} .into_owned();
} }
path.to_string() path.to_string()
} }
@@ -312,11 +312,11 @@ impl Config {
if let Some(ref mut path) = self.database.path { if let Some(ref mut path) = self.database.path {
*path = resolve_home_path(path); *path = resolve_home_path(path);
} }
if let Some(ref mut url) = self.database.url { if let Some(ref mut url) = self.database.url
if let Some(stripped) = url.strip_prefix("sqlite://") { && let Some(stripped) = url.strip_prefix("sqlite://")
let clean = resolve_home_path(stripped); {
*url = format!("sqlite://{clean}"); let clean = resolve_home_path(stripped);
} *url = format!("sqlite://{clean}");
} }
} }
@@ -372,10 +372,10 @@ impl Config {
/// Default user configuration path (~/.config/nx9-auth/config.toml) /// Default user configuration path (~/.config/nx9-auth/config.toml)
pub fn default_user_config_path() -> Option<PathBuf> { pub fn default_user_config_path() -> Option<PathBuf> {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME")
if !xdg.is_empty() { && !xdg.is_empty()
return Some(PathBuf::from(xdg).join("nx9-auth/config.toml")); {
} return Some(PathBuf::from(xdg).join("nx9-auth/config.toml"));
} }
if let Ok(home) = std::env::var("HOME") { if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home).join(".config/nx9-auth/config.toml")); return Some(PathBuf::from(home).join(".config/nx9-auth/config.toml"));
+12 -12
View File
@@ -57,12 +57,12 @@ pub async fn init_provider(
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
DatabaseBackend::Sqlite => { DatabaseBackend::Sqlite => {
let path = config.database.sqlite_path(); let path = config.database.sqlite_path();
if let Some(parent) = std::path::Path::new(&path).parent() { if let Some(parent) = std::path::Path::new(&path).parent()
if !parent.as_os_str().is_empty() { && !parent.as_os_str().is_empty()
std::fs::create_dir_all(parent).with_context(|| { {
format!("failed to create database directory: {}", parent.display()) std::fs::create_dir_all(parent).with_context(|| {
})?; format!("failed to create database directory: {}", parent.display())
} })?;
} }
let max_conn = config.database.max_connections.unwrap_or(16); let max_conn = config.database.max_connections.unwrap_or(16);
@@ -177,12 +177,12 @@ pub async fn init_provider(
/// Helper function to create an SQLite pool for legacy CLI commands or tests. /// Helper function to create an SQLite pool for legacy CLI commands or tests.
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
pub async fn create_pool(path: &str) -> Result<SqlitePool> { pub async fn create_pool(path: &str) -> Result<SqlitePool> {
if let Some(parent) = std::path::Path::new(path).parent() { if let Some(parent) = std::path::Path::new(path).parent()
if !parent.as_os_str().is_empty() { && !parent.as_os_str().is_empty()
std::fs::create_dir_all(parent).with_context(|| { {
format!("failed to create database directory: {}", parent.display()) std::fs::create_dir_all(parent).with_context(|| {
})?; format!("failed to create database directory: {}", parent.display())
} })?;
} }
let url = if path.starts_with("sqlite://") { let url = if path.starts_with("sqlite://") {
path.to_string() path.to_string()
+31 -31
View File
@@ -221,40 +221,40 @@ pub async fn update(
} }
} }
if let Some(new_enabled) = enabled { if let Some(new_enabled) = enabled
if new_enabled != existing.enabled { && new_enabled != existing.enabled
let action = if new_enabled { {
"application.member_enabled" let action = if new_enabled {
} else { "application.member_enabled"
"application.member_disabled" } else {
}; "application.member_disabled"
};
let metadata = serde_json::json!({ let metadata = serde_json::json!({
"application_id": application_id, "application_id": application_id,
"user_id": user_id, "user_id": user_id,
"role": existing.role, "role": existing.role,
"enabled": new_enabled, "enabled": new_enabled,
}) })
.to_string(); .to_string();
let audit_event = crate::audit::AuditEvent { let audit_event = crate::audit::AuditEvent {
actor_id: audit_actor_id, actor_id: audit_actor_id,
target_id: Some(user_id), target_id: Some(user_id),
action, action,
resource_type: "application", resource_type: "application",
resource_id: Some(application_id), resource_id: Some(application_id),
severity: crate::db::models::AuditSeverity::Info, severity: crate::db::models::AuditSeverity::Info,
ip: audit_ip, ip: audit_ip,
ua: audit_ua, ua: audit_ua,
metadata: Some(&metadata), metadata: Some(&metadata),
}; };
provider provider
.application_members() .application_members()
.set_enabled_with_audit(application_id, user_id, new_enabled, Some(audit_event)) .set_enabled_with_audit(application_id, user_id, new_enabled, Some(audit_event))
.await .await
.map_err(AppError::Database)?; .map_err(AppError::Database)?;
}
} }
provider provider
+2 -3
View File
@@ -324,10 +324,9 @@ pub async fn update(
.find_by_slug(slug) .find_by_slug(slug)
.await .await
.map_err(AppError::Database)? .map_err(AppError::Database)?
&& (other.entity_id != id || other.entity_type != "application")
{ {
if other.entity_id != id || other.entity_type != "application" { return Err(AppError::Conflict(format!("slug '{slug}' already exists")));
return Err(AppError::Conflict(format!("slug '{slug}' already exists")));
}
} }
let redirect_json = redirect_uris.map(|v| serde_json::to_string(&v).unwrap_or_default()); let redirect_json = redirect_uris.map(|v| serde_json::to_string(&v).unwrap_or_default());
+2 -3
View File
@@ -191,10 +191,9 @@ pub async fn update_role(
.find_by_name(name) .find_by_name(name)
.await .await
.map_err(AppError::Database)? .map_err(AppError::Database)?
&& other.id != id
{ {
if other.id != id { return Err(AppError::Conflict(format!("role '{name}' already exists")));
return Err(AppError::Conflict(format!("role '{name}' already exists")));
}
} }
provider provider
+46 -50
View File
@@ -72,59 +72,55 @@ where
// 2. Try Authorization: Bearer — PAT first, then session token. // 2. Try Authorization: Bearer — PAT first, then session token.
// Session tokens are returned from /auth/login for SPA clients that // Session tokens are returned from /auth/login for SPA clients that
// cannot rely solely on the HttpOnly cookie. // cannot rely solely on the HttpOnly cookie.
if let Some(auth_header) = parts.headers.get(axum::http::header::AUTHORIZATION) { if let Some(auth_header) = parts.headers.get(axum::http::header::AUTHORIZATION)
if let Ok(value) = auth_header.to_str() { && let Ok(value) = auth_header.to_str()
if let Some(raw) = value.strip_prefix("Bearer ") { && let Some(raw) = value.strip_prefix("Bearer ")
let raw = raw.trim(); {
let raw = raw.trim();
// 2a. Personal access token // 2a. Personal access token
if let Some(token) = tokens::validate_token(&app_state.provider, raw).await? { if let Some(token) = tokens::validate_token(&app_state.provider, raw).await? {
let user = app_state let user = app_state
.provider .provider
.users() .users()
.find_by_id(&token.user_id) .find_by_id(&token.user_id)
.await .await
.map_err(AppError::Database)? .map_err(AppError::Database)?
.ok_or(AppError::Unauthorized)?; .ok_or(AppError::Unauthorized)?;
if !user.is_active() { if !user.is_active() {
return Err(AppError::Unauthorized); return Err(AppError::Unauthorized);
}
return Ok(AuthUser {
user,
method: AuthMethod::Token,
session_id: None,
});
}
// 2b. Session token (same value as nx9_session cookie)
if let Some(session) = sessions::validate_session(
&app_state.provider,
raw,
&app_state.config.security,
)
.await?
{
let user = app_state
.provider
.users()
.find_by_id(&session.user_id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::Unauthorized)?;
if !user.is_active() {
return Err(AppError::Unauthorized);
}
return Ok(AuthUser {
user,
method: AuthMethod::Session,
session_id: Some(session.id),
});
}
} }
return Ok(AuthUser {
user,
method: AuthMethod::Token,
session_id: None,
});
}
// 2b. Session token (same value as nx9_session cookie)
if let Some(session) =
sessions::validate_session(&app_state.provider, raw, &app_state.config.security)
.await?
{
let user = app_state
.provider
.users()
.find_by_id(&session.user_id)
.await
.map_err(AppError::Database)?
.ok_or(AppError::Unauthorized)?;
if !user.is_active() {
return Err(AppError::Unauthorized);
}
return Ok(AuthUser {
user,
method: AuthMethod::Session,
session_id: Some(session.id),
});
} }
} }
+6 -6
View File
@@ -162,12 +162,12 @@ impl Lifecycle for Application {
self.pool_handle = Some(pool_handle); self.pool_handle = Some(pool_handle);
} }
if self.router.is_none() { if self.router.is_none()
if let Some(provider) = &self.provider { && let Some(provider) = &self.provider
let app_state = crate::state::AppState::new(provider.clone(), config); {
let router = crate::api::router::build(app_state); let app_state = crate::state::AppState::new(provider.clone(), config);
self.router = Some(router); let router = crate::api::router::build(app_state);
} self.router = Some(router);
} }
Ok(()) Ok(())
+31 -14
View File
@@ -17,10 +17,10 @@ pub fn hash_password(password: &str, cfg: &SecurityConfig) -> Result<String, App
cfg.argon2_parallelism, cfg.argon2_parallelism,
None, None,
) )
.map_err(|e| { .map_err(|e| {
tracing::error!(error = %e, "invalid argon2 params"); tracing::error!(error = %e, "invalid argon2 params");
AppError::Internal AppError::Internal
})?, })?,
); );
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
@@ -55,7 +55,10 @@ pub fn verify_password(password: &str, hash: &str) -> Result<bool, AppError> {
pub fn verify_dummy(cfg: &SecurityConfig) -> Result<(), AppError> { pub fn verify_dummy(cfg: &SecurityConfig) -> Result<(), AppError> {
// We hash a constant string to ensure the time taken is consistent // We hash a constant string to ensure the time taken is consistent
// and aligns with the cost parameters defined in the config. // and aligns with the cost parameters defined in the config.
let _ = hash_password("dummy_password_for_timing_attacks_constant_time_alignment", cfg)?; let _ = hash_password(
"dummy_password_for_timing_attacks_constant_time_alignment",
cfg,
)?;
Ok(()) Ok(())
} }
@@ -70,8 +73,8 @@ pub fn validate_password_strength(password: &str, is_admin: bool) -> Result<(),
let normalized = password.to_lowercase(); let normalized = password.to_lowercase();
// Use a HashSet for O(1) exact matching against compromised/weak passwords // 1. Exact matches for highly common passwords
let weak_passwords: HashSet<&str> = [ let exact_weak: HashSet<&str> = [
"password", "password",
"admin123", "admin123",
"qwerty", "qwerty",
@@ -84,23 +87,37 @@ pub fn validate_password_strength(password: &str, is_admin: bool) -> Result<(),
"admin", "admin",
"letmein", "letmein",
"welcome", "welcome",
"password12345",
] ]
.iter() .into_iter()
.copied() .collect();
.collect();
if weak_passwords.contains(normalized.as_str()) { if exact_weak.contains(normalized.as_str()) {
return Err(AppError::InvalidInput( return Err(AppError::InvalidInput(
"password is too common or weak".to_string(), "password is too common or weak".to_string(),
)); ));
} }
// Additional check: reject if it's a simple sequence or pattern // 2. Substring matches for highly restricted roots
if password.chars().all(|c| c == password.chars().next().unwrap()) { // We ban these substrings because "password12345" or "qwerty2024" are trivially guessable.
let banned_substrings = ["password", "qwerty", "123456"];
for banned in &banned_substrings {
if normalized.contains(banned) {
return Err(AppError::InvalidInput(
"password contains a restricted sequence".to_string(),
));
}
}
// 3. Reject single repeated characters
if password
.chars()
.all(|c| c == password.chars().next().unwrap())
{
return Err(AppError::InvalidInput( return Err(AppError::InvalidInput(
"password cannot be a single repeated character".to_string(), "password cannot be a single repeated character".to_string(),
)); ));
} }
Ok(()) Ok(())
} }
+11 -12
View File
@@ -31,7 +31,7 @@ impl IpState {
/// Returns true if this state is no longer active and can be removed. /// Returns true if this state is no longer active and can be removed.
fn is_stale(&self, now: Instant) -> bool { fn is_stale(&self, now: Instant) -> bool {
self.window.is_empty() && self.locked_until.map_or(true, |until| now >= until) self.window.is_empty() && self.locked_until.is_none_or(|until| now >= until)
} }
} }
@@ -71,12 +71,11 @@ impl RateLimiter {
/// Check if the given IP is currently allowed to attempt a login. /// Check if the given IP is currently allowed to attempt a login.
pub fn check(&self, ip: IpAddr) -> Result<(), AppError> { pub fn check(&self, ip: IpAddr) -> Result<(), AppError> {
if let Some(s) = self.state.get(&ip) { if let Some(s) = self.state.get(&ip)
if let Some(until) = s.locked_until { && let Some(until) = s.locked_until
if Instant::now() < until { && Instant::now() < until
return Err(AppError::RateLimited); {
} return Err(AppError::RateLimited);
}
} }
Ok(()) Ok(())
} }
@@ -87,10 +86,10 @@ impl RateLimiter {
let now = Instant::now(); let now = Instant::now();
// Clear the lockout if it has expired // Clear the lockout if it has expired
if let Some(until) = s.locked_until { if let Some(until) = s.locked_until
if now >= until { && now >= until
s.locked_until = None; {
} s.locked_until = None;
} }
// Prune old failures outside the window // Prune old failures outside the window
@@ -142,4 +141,4 @@ impl Default for RateLimiter {
max_failures: 5, max_failures: 5,
} }
} }
} }
+9 -9
View File
@@ -77,15 +77,15 @@ pub async fn validate_session(
let now = chrono::Utc::now(); let now = chrono::Utc::now();
// Check absolute expiry // Check absolute expiry
if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(&session.expires_at) { if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(&session.expires_at)
if now > expires { && now > expires
provider {
.sessions() provider
.revoke(&session.id) .sessions()
.await .revoke(&session.id)
.map_err(AppError::Database)?; .await
return Ok(None); .map_err(AppError::Database)?;
} return Ok(None);
} }
// Check idle timeout // Check idle timeout
+5 -6
View File
@@ -130,12 +130,11 @@ pub async fn validate_token(
}; };
// Check expiry if set // Check expiry if set
if let Some(ref exp) = token.expires_at { if let Some(ref exp) = token.expires_at
if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(exp) { && let Ok(expires) = chrono::DateTime::parse_from_rfc3339(exp)
if chrono::Utc::now() > expires { && chrono::Utc::now() > expires
return Ok(None); {
} return Ok(None);
}
} }
// Touch last_used_at (fire-and-forget) // Touch last_used_at (fire-and-forget)
+4 -1
View File
@@ -73,7 +73,10 @@ fn test_config(db_path: String) -> Config {
async fn test_security_no_plaintext_passwords_in_db() { async fn test_security_no_plaintext_passwords_in_db() {
let (provider, pool, db_path) = setup_test_db().await; let (provider, pool, db_path) = setup_test_db().await;
let sec_cfg = test_security_config(); let sec_cfg = test_security_config();
let password = "super_secret_special_pass_123456";
//let password = "super_secret_special_pass_123456";
// Use a strong, random string to bypass the restricted sequence validator
let password = "Xy7#bN9@mK2$pQ5!vL8&zW4";
let user = identity_users::create_user( let user = identity_users::create_user(
&provider, &provider,
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nx9-auth-ui" name = "nx9-auth-ui"
version = "0.3.0" version = "0.4.0"
edition = "2024" edition = "2024"
authors = ["NX9 Team", "Sunil Thakare"] authors = ["NX9 Team", "Sunil Thakare"]
description = "Dioxus web UI for nx9-auth IAM" description = "Dioxus web UI for nx9-auth IAM"