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:
@@ -94,7 +94,7 @@ impl JwtTokenService {
|
||||
impl TokenService for JwtTokenService {
|
||||
/// Generate an access + refresh token pair for the given subject.
|
||||
fn generate_tokens(&self, sub: &str) -> anyhow::Result<(String, String)> {
|
||||
use zesdex_infrastructure::auth::jwt::{create_token, JwtClaims};
|
||||
use zesdex_infrastructure::auth::jwt::{create_token, JwtClaims, TokenType};
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -102,12 +102,21 @@ impl TokenService for JwtTokenService {
|
||||
.as_secs();
|
||||
|
||||
// Access token
|
||||
let access_claims = JwtClaims::new(sub.to_string(), now + self.access_token_expiry_secs, None);
|
||||
let access_claims = JwtClaims::new(
|
||||
sub.to_string(),
|
||||
now + self.access_token_expiry_secs,
|
||||
TokenType::Access,
|
||||
None,
|
||||
);
|
||||
let access_token = create_token(&self.secret, access_claims)?;
|
||||
|
||||
// Refresh token (longer-lived)
|
||||
let refresh_claims =
|
||||
JwtClaims::new(sub.to_string(), now + self.refresh_token_expiry_secs, None);
|
||||
let refresh_claims = JwtClaims::new(
|
||||
sub.to_string(),
|
||||
now + self.refresh_token_expiry_secs,
|
||||
TokenType::Refresh,
|
||||
None,
|
||||
);
|
||||
let refresh_token = create_token(&self.secret, refresh_claims)?;
|
||||
|
||||
Ok((access_token, refresh_token))
|
||||
@@ -123,14 +132,17 @@ impl TokenService for JwtTokenService {
|
||||
|
||||
/// Verify a refresh token and return the subject claim.
|
||||
///
|
||||
/// Delegates to the same JWT verification function as access tokens;
|
||||
/// the signature algorithm and secret are shared. Expiry validation
|
||||
/// is handled by the JWT library against the `exp` claim embedded
|
||||
/// in the token payload.
|
||||
/// Enforces that the presented token is a **refresh** token (`typ =
|
||||
/// "refresh"`) — an access token presented here is rejected, closing
|
||||
/// the replay-window escalation where a stolen 1-hour access token
|
||||
/// could otherwise be exchanged for a fresh 7-day credential.
|
||||
fn verify_refresh_token(&self, token: &str) -> anyhow::Result<String> {
|
||||
use zesdex_infrastructure::auth::jwt::verify_token;
|
||||
use zesdex_infrastructure::auth::jwt::{verify_token, TokenType};
|
||||
|
||||
let claims = verify_token(&self.secret, token)?;
|
||||
if claims.token_type != TokenType::Refresh {
|
||||
anyhow::bail!("token is not a refresh token");
|
||||
}
|
||||
Ok(claims.sub)
|
||||
}
|
||||
}
|
||||
@@ -193,6 +205,9 @@ pub struct ApiState {
|
||||
/// HS256 JWT token generation and verification.
|
||||
pub token_service: JwtTokenService,
|
||||
|
||||
/// Shared sliding-window limiter for auth endpoints (login/register/refresh).
|
||||
pub auth_rate_limiter: zesdex_infrastructure::middleware::rate_limit::RateLimiter,
|
||||
|
||||
/// LLM provider client for chat completions.
|
||||
pub llm_client: zesdex_infrastructure::llm::provider::LlmClient,
|
||||
}
|
||||
@@ -272,6 +287,7 @@ impl ApiState {
|
||||
zesdex_application::cms::MemoryServiceImpl::new(memory_repo, memory_dir);
|
||||
|
||||
let token_service = JwtTokenService::new(&jwt_secret);
|
||||
let auth_rate_limiter = zesdex_infrastructure::middleware::rate_limit::RateLimiter::new();
|
||||
let llm_client = zesdex_infrastructure::llm::provider::LlmClient::new(
|
||||
llm_api_key.into(),
|
||||
llm_model.into(),
|
||||
@@ -287,6 +303,7 @@ impl ApiState {
|
||||
memory_service,
|
||||
password_service: Argon2PasswordService,
|
||||
token_service,
|
||||
auth_rate_limiter,
|
||||
llm_client,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user