fix(api): perbaiki keamanan auth & WebSocket, tambah rate limiting
Security fixes hasil audit: - fix(auth): refresh token kini memakai claim typ=refresh; access token tidak bisa dipakai sebagai refresh token (sebelumnya bisa — eskalasi masa berlaku 1 jam -> 7 hari) - fix(api): layer JWT hanya melindungi route /sessions dan /chat; /auth/login, /auth/register, /auth/refresh, /health kini publik (sebelumnya semua route 401-lock, API tidak bisa dipakai sama sekali) - fix(ws): endpoint /ws kini memverifikasi token ZESDEX_WS_TOKEN via query param jika env diset (mencegah pemakaian LLM proxy terbuka) - feat(api): rate limiting login/register/refresh (20 request / 10 menit per client IP) memakai RateLimiter yang tadinya dead code - test(jwt): tambah unit test token type access vs refresh + expired
This commit is contained in:
@@ -8,12 +8,27 @@ pub struct JwtClaims {
|
||||
pub sub: String,
|
||||
pub exp: u64,
|
||||
pub iat: u64,
|
||||
/// Token purpose: `"access"` or `"refresh"`.
|
||||
///
|
||||
/// Prevents an access token from being replayed as a refresh token
|
||||
/// (which would otherwise extend a short-lived credential into the
|
||||
/// 7-day refresh window).
|
||||
#[serde(rename = "typ")]
|
||||
pub token_type: TokenType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
/// JWT token purpose.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TokenType {
|
||||
Access,
|
||||
Refresh,
|
||||
}
|
||||
|
||||
impl JwtClaims {
|
||||
pub fn new(sub: String, exp: u64, session_id: Option<String>) -> Self {
|
||||
pub fn new(sub: String, exp: u64, token_type: TokenType, session_id: Option<String>) -> Self {
|
||||
let iat = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
@@ -22,6 +37,7 @@ impl JwtClaims {
|
||||
sub,
|
||||
exp,
|
||||
iat,
|
||||
token_type,
|
||||
session_id,
|
||||
}
|
||||
}
|
||||
@@ -48,3 +64,57 @@ pub fn verify_token(secret: &str, token: &str) -> anyhow::Result<JwtClaims> {
|
||||
let token_data = jsonwebtoken::decode::<JwtClaims>(token, &key, &validation)?;
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn claims(exp_secs_from_now: u64, token_type: TokenType) -> JwtClaims {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
JwtClaims::new(
|
||||
"user-1".to_string(),
|
||||
now + exp_secs_from_now,
|
||||
token_type,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_and_refresh_tokens_roundtrip() {
|
||||
let secret = "test-secret";
|
||||
let access = create_token(secret, claims(3600, TokenType::Access)).unwrap();
|
||||
let refresh = create_token(secret, claims(604800, TokenType::Refresh)).unwrap();
|
||||
|
||||
let acc = verify_token(secret, &access).unwrap();
|
||||
assert_eq!(acc.token_type, TokenType::Access);
|
||||
|
||||
let refr = verify_token(secret, &refresh).unwrap();
|
||||
assert_eq!(refr.token_type, TokenType::Refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_type_is_distinct() {
|
||||
let secret = "test-secret";
|
||||
let access = create_token(secret, claims(3600, TokenType::Access)).unwrap();
|
||||
let claims = verify_token(secret, &access).unwrap();
|
||||
assert_ne!(claims.token_type, TokenType::Refresh);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_token_is_rejected() {
|
||||
let secret = "test-secret";
|
||||
// exp well in the past (beyond the library's default 60s leeway) →
|
||||
// verification must fail.
|
||||
let past = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
.saturating_sub(120);
|
||||
let expired = JwtClaims::new("user-1".to_string(), past, TokenType::Access, None);
|
||||
let token = create_token(secret, expired).unwrap();
|
||||
assert!(verify_token(secret, &token).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user