fix(auth): validate JWT secret length, equalize login timing, reduce TTL to 24h
This commit is contained in:
@@ -95,7 +95,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_and_validate_token() {
|
fn generate_and_validate_token() {
|
||||||
let svc = JwtAuthService::new("secret".into(), 3600);
|
let svc = JwtAuthService::new("a-secret-that-is-at-least-32-bytes!!".into(), 3600);
|
||||||
let id = UserId::new();
|
let id = UserId::new();
|
||||||
let tok = svc.generate_token(&id).unwrap();
|
let tok = svc.generate_token(&id).unwrap();
|
||||||
let parsed = svc.validate_token(&tok.token).unwrap();
|
let parsed = svc.validate_token(&tok.token).unwrap();
|
||||||
@@ -104,7 +104,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_token_returns_unauthorized() {
|
fn invalid_token_returns_unauthorized() {
|
||||||
let svc = JwtAuthService::new("secret".into(), 3600);
|
let svc = JwtAuthService::new("a-secret-that-is-at-least-32-bytes!!".into(), 3600);
|
||||||
let err = svc.validate_token("not.a.token").unwrap_err();
|
let err = svc.validate_token("not.a.token").unwrap_err();
|
||||||
assert!(matches!(err, DomainError::Unauthorized));
|
assert!(matches!(err, DomainError::Unauthorized));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,15 @@ pub async fn login(
|
|||||||
input: LoginInput,
|
input: LoginInput,
|
||||||
) -> Result<LoginOutput, DomainError> {
|
) -> Result<LoginOutput, DomainError> {
|
||||||
let email = Email::new(input.email)?;
|
let email = Email::new(input.email)?;
|
||||||
let user = users
|
let user = users.find_by_email(&email).await?;
|
||||||
.find_by_email(&email)
|
if user.is_none() {
|
||||||
.await?
|
// Timing equalization — prevents email enumeration via response-time oracle.
|
||||||
.ok_or(DomainError::Unauthorized)?;
|
// Running the hasher on a miss makes "no such user" take the same time as
|
||||||
|
// "wrong password", so attackers cannot distinguish the two cases.
|
||||||
|
let _ = hasher.hash(&input.password).await;
|
||||||
|
return Err(DomainError::Unauthorized);
|
||||||
|
}
|
||||||
|
let user = user.unwrap();
|
||||||
if !hasher.verify(&input.password, &user.password_hash).await? {
|
if !hasher.verify(&input.password, &user.password_hash).await? {
|
||||||
return Err(DomainError::Unauthorized);
|
return Err(DomainError::Unauthorized);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const JWT_TTL_SECS: i64 = 86_400 * 30;
|
const JWT_TTL_SECS: i64 = 86_400; // 24 hours (was 30 days)
|
||||||
|
const JWT_SECRET_MIN_BYTES: usize = 32; // 256 bits minimum for HS256
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -107,10 +108,16 @@ pub async fn build(cfg: &Config) -> Infrastructure {
|
|||||||
)),
|
)),
|
||||||
feed: Arc::new(postgres::feed::PgFeedRepository::new(pool.clone())),
|
feed: Arc::new(postgres::feed::PgFeedRepository::new(pool.clone())),
|
||||||
search: Arc::new(postgres_search::PgSearchRepository::new(pool.clone())),
|
search: Arc::new(postgres_search::PgSearchRepository::new(pool.clone())),
|
||||||
auth: Arc::new(auth::JwtAuthService::new(
|
auth: Arc::new({
|
||||||
cfg.jwt_secret.clone(),
|
if cfg.jwt_secret.len() < JWT_SECRET_MIN_BYTES {
|
||||||
JWT_TTL_SECS,
|
panic!(
|
||||||
)),
|
"JWT_SECRET is {} bytes — minimum is {} bytes for HS256 security",
|
||||||
|
cfg.jwt_secret.len(),
|
||||||
|
JWT_SECRET_MIN_BYTES,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
auth::JwtAuthService::new(cfg.jwt_secret.clone(), JWT_TTL_SECS)
|
||||||
|
}),
|
||||||
hasher: Arc::new(auth::Argon2PasswordHasher),
|
hasher: Arc::new(auth::Argon2PasswordHasher),
|
||||||
events: event_publisher,
|
events: event_publisher,
|
||||||
federation: ap_service.clone() as Arc<dyn domain::ports::FederationActionPort>,
|
federation: ap_service.clone() as Arc<dyn domain::ports::FederationActionPort>,
|
||||||
|
|||||||
Reference in New Issue
Block a user