mirror of
https://github.com/DaLaw2/NetGuardia.git
synced 2026-08-24 14:10:28 +09:00
feat: JWT invalidation on restart, frontend inactivity auto-logout
JWT restart invalidation: - Generate fresh random signing secret on every boot instead of persisting - All existing tokens become invalid after server restart - Remove hex_encode/hex_decode utilities (no longer needed) Frontend inactivity auto-logout (submodule update): - 30-minute idle timeout on user interaction events - Auto-redirects to /login when idle timeout fires Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a7f74999ff
commit
91dfa7924a
@ -1 +1 @@
|
||||
Subproject commit 71d2d7f2d53f4afe6510b3018227aa5e28d97476
|
||||
Subproject commit dbe5342900202407fa47ad2e8062326cb0608846
|
||||
@ -14,20 +14,15 @@ pub struct JwtService {
|
||||
}
|
||||
|
||||
impl JwtService {
|
||||
pub fn new(secrets: &Arc<dyn SecretStorePort>, expiry_hours: u64) -> Result<Self, Error> {
|
||||
let raw_bytes = match secrets.get_secret("jwt_secret")? {
|
||||
Some(hex_str) => hex_decode(&hex_str).map_err(|_| AuthError::InvalidToken)?,
|
||||
None => {
|
||||
use rand::Rng;
|
||||
let secret: [u8; 32] = rand::rng().random();
|
||||
secrets.set_secret("jwt_secret", &hex_encode(&secret))?;
|
||||
secret.to_vec()
|
||||
}
|
||||
};
|
||||
/// Generate a fresh random JWT signing secret on every boot.
|
||||
/// This intentionally invalidates all existing tokens on restart.
|
||||
pub fn new(_secrets: &Arc<dyn SecretStorePort>, expiry_hours: u64) -> Result<Self, Error> {
|
||||
use rand::Rng;
|
||||
let secret: [u8; 32] = rand::rng().random();
|
||||
|
||||
Ok(Self {
|
||||
encoding_key: EncodingKey::from_secret(&raw_bytes),
|
||||
decoding_key: DecodingKey::from_secret(&raw_bytes),
|
||||
encoding_key: EncodingKey::from_secret(&secret),
|
||||
decoding_key: DecodingKey::from_secret(&secret),
|
||||
expiry_hours,
|
||||
})
|
||||
}
|
||||
@ -67,25 +62,6 @@ impl JwtService {
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_encode(data: &[u8]) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut s = String::with_capacity(data.len() * 2);
|
||||
for b in data {
|
||||
write!(s, "{:02x}", b).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn hex_decode(hex: &str) -> Result<Vec<u8>, &'static str> {
|
||||
if !hex.len().is_multiple_of(2) {
|
||||
return Err("odd-length hex string");
|
||||
}
|
||||
(0..hex.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).map_err(|_| "invalid hex"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -137,18 +113,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jwt_secret_persistence() {
|
||||
fn test_jwt_secret_changes_on_new_instance() {
|
||||
let db = Arc::new(Database::new(":memory:").unwrap());
|
||||
let secrets: Arc<dyn SecretStorePort> = Arc::new(SecretStore::new(db));
|
||||
|
||||
// First creation generates and stores secret
|
||||
let jwt1 = JwtService::new(&secrets, 24).unwrap();
|
||||
let token = jwt1.create_token(1, "admin", "admin", vec![]).unwrap();
|
||||
|
||||
// Second creation reuses stored secret
|
||||
// New instance = new secret = old token invalid (simulates restart)
|
||||
let jwt2 = JwtService::new(&secrets, 24).unwrap();
|
||||
let claims = jwt2.validate_token(&token).unwrap();
|
||||
assert_eq!(claims.username, "admin");
|
||||
let result = jwt2.validate_token(&token);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -161,35 +136,4 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_decode_valid() {
|
||||
let result = hex_decode("48656c6c6f").unwrap();
|
||||
assert_eq!(result, b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_decode_empty() {
|
||||
let result = hex_decode("").unwrap();
|
||||
assert_eq!(result, Vec::<u8>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_decode_odd_length() {
|
||||
let result = hex_decode("abc");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_decode_invalid_chars() {
|
||||
let result = hex_decode("gg");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hex_roundtrip() {
|
||||
let data = b"NetGuardia\x00\xff";
|
||||
let encoded = hex_encode(data);
|
||||
let decoded = hex_decode(&encoded).unwrap();
|
||||
assert_eq!(decoded, data);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user