Skip to content

Commit 15e30c7

Browse files
committed
Validate JWT algorithm allowlist is non-empty at construction
1 parent 348f588 commit 15e30c7

4 files changed

Lines changed: 47 additions & 35 deletions

File tree

src/auth/gateway_jwt.rs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const NEGATIVE_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(6
2323
/// Maximum number of negative cache entries before eviction kicks in.
2424
/// Prevents unbounded memory growth from attacker-controlled JWT issuers.
2525
#[cfg(feature = "sso")]
26-
const MAX_NEGATIVE_CACHE_ENTRIES: usize = 10_000;
26+
const MAX_NEGATIVE_CACHE_ENTRIES: usize = 1_000;
2727

2828
/// Internal state behind the single `RwLock`.
2929
struct RegistryInner {
@@ -94,7 +94,7 @@ impl GatewayJwtRegistry {
9494
super::fetch_jwks_uri(discovery_url, http_client, allow_loopback, allow_private)
9595
.await?;
9696
let jwt_config = build_jwt_config_from_sso(issuer, client_id, &jwks_url, config);
97-
let validator = Arc::new(JwtValidator::with_client(jwt_config, http_client.clone()));
97+
let validator = Arc::new(JwtValidator::with_client(jwt_config, http_client.clone())?);
9898

9999
// Single write lock: remove old issuer index, insert validator, update index
100100
let mut inner = self.inner.write().await;
@@ -318,7 +318,7 @@ mod tests {
318318
allowed_algorithms: vec![JwtAlgorithm::RS256],
319319
};
320320

321-
let validator = Arc::new(JwtValidator::new(config));
321+
let validator = Arc::new(JwtValidator::new(config).unwrap());
322322
{
323323
let mut inner = registry.inner.write().await;
324324
inner.validators.insert(org_id, validator);
@@ -357,17 +357,20 @@ mod tests {
357357
let org2 = Uuid::new_v4();
358358

359359
let make_validator = || {
360-
Arc::new(JwtValidator::new(JwtAuthConfig {
361-
issuer: issuer.to_string(),
362-
audience: OneOrMany::One("test".to_string()),
363-
jwks_url: "https://shared-idp.example.com/jwks".to_string(),
364-
jwks_refresh_secs: 3600,
365-
identity_claim: "sub".to_string(),
366-
org_claim: None,
367-
additional_claims: vec![],
368-
allow_expired: false,
369-
allowed_algorithms: vec![JwtAlgorithm::RS256],
370-
}))
360+
Arc::new(
361+
JwtValidator::new(JwtAuthConfig {
362+
issuer: issuer.to_string(),
363+
audience: OneOrMany::One("test".to_string()),
364+
jwks_url: "https://shared-idp.example.com/jwks".to_string(),
365+
jwks_refresh_secs: 3600,
366+
identity_claim: "sub".to_string(),
367+
org_claim: None,
368+
additional_claims: vec![],
369+
allow_expired: false,
370+
allowed_algorithms: vec![JwtAlgorithm::RS256],
371+
})
372+
.unwrap(),
373+
)
371374
};
372375

373376
{

src/auth/jwt.rs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -114,21 +114,34 @@ pub struct JwtValidator {
114114
impl JwtValidator {
115115
/// Create a new JWT validator.
116116
#[allow(dead_code)] // Auth infrastructure
117-
pub fn new(config: JwtAuthConfig) -> Self {
118-
Self {
117+
pub fn new(config: JwtAuthConfig) -> Result<Self, AuthError> {
118+
if config.allowed_algorithms.is_empty() {
119+
return Err(AuthError::Internal(
120+
"JWT allowed_algorithms must not be empty".into(),
121+
));
122+
}
123+
Ok(Self {
119124
config,
120125
http_client: reqwest::Client::new(),
121126
jwks_cache: RwLock::new(None),
122-
}
127+
})
123128
}
124129

125130
/// Create a new JWT validator with a custom HTTP client.
126-
pub fn with_client(config: JwtAuthConfig, http_client: reqwest::Client) -> Self {
127-
Self {
131+
pub fn with_client(
132+
config: JwtAuthConfig,
133+
http_client: reqwest::Client,
134+
) -> Result<Self, AuthError> {
135+
if config.allowed_algorithms.is_empty() {
136+
return Err(AuthError::Internal(
137+
"JWT allowed_algorithms must not be empty".into(),
138+
));
139+
}
140+
Ok(Self {
128141
config,
129142
http_client,
130143
jwks_cache: RwLock::new(None),
131-
}
144+
})
132145
}
133146

134147
/// Validate a JWT and return the claims.
@@ -423,23 +436,23 @@ mod tests {
423436
#[test]
424437
fn test_algorithm_allowlist_rs256_allowed() {
425438
let config = test_config();
426-
let validator = JwtValidator::new(config);
439+
let validator = JwtValidator::new(config).unwrap();
427440

428441
assert!(validator.is_algorithm_allowed(Algorithm::RS256));
429442
}
430443

431444
#[test]
432445
fn test_algorithm_allowlist_es256_allowed() {
433446
let config = test_config();
434-
let validator = JwtValidator::new(config);
447+
let validator = JwtValidator::new(config).unwrap();
435448

436449
assert!(validator.is_algorithm_allowed(Algorithm::ES256));
437450
}
438451

439452
#[test]
440453
fn test_algorithm_allowlist_hs256_rejected() {
441454
let config = test_config();
442-
let validator = JwtValidator::new(config);
455+
let validator = JwtValidator::new(config).unwrap();
443456

444457
// HS256 is not in the allowed list
445458
assert!(!validator.is_algorithm_allowed(Algorithm::HS256));
@@ -448,7 +461,7 @@ mod tests {
448461
#[test]
449462
fn test_algorithm_allowlist_rs384_rejected() {
450463
let config = test_config();
451-
let validator = JwtValidator::new(config);
464+
let validator = JwtValidator::new(config).unwrap();
452465

453466
// RS384 is not in the allowed list (only RS256 and ES256)
454467
assert!(!validator.is_algorithm_allowed(Algorithm::RS384));
@@ -461,7 +474,7 @@ mod tests {
461474
allowed_algorithms: vec![JwtAlgorithm::HS256],
462475
..test_config()
463476
};
464-
let validator = JwtValidator::new(config);
477+
let validator = JwtValidator::new(config).unwrap();
465478

466479
assert!(validator.is_algorithm_allowed(Algorithm::HS256));
467480
assert!(!validator.is_algorithm_allowed(Algorithm::RS256));
@@ -477,7 +490,7 @@ mod tests {
477490
],
478491
..test_config()
479492
};
480-
let validator = JwtValidator::new(config);
493+
let validator = JwtValidator::new(config).unwrap();
481494

482495
assert!(validator.is_algorithm_allowed(Algorithm::RS256));
483496
assert!(validator.is_algorithm_allowed(Algorithm::RS384));
@@ -487,16 +500,12 @@ mod tests {
487500
}
488501

489502
#[test]
490-
fn test_algorithm_allowlist_empty_rejects_all() {
503+
fn test_algorithm_allowlist_empty_rejected() {
491504
let config = JwtAuthConfig {
492505
allowed_algorithms: vec![],
493506
..test_config()
494507
};
495-
let validator = JwtValidator::new(config);
496-
497-
assert!(!validator.is_algorithm_allowed(Algorithm::RS256));
498-
assert!(!validator.is_algorithm_allowed(Algorithm::ES256));
499-
assert!(!validator.is_algorithm_allowed(Algorithm::HS256));
508+
assert!(JwtValidator::new(config).is_err());
500509
}
501510

502511
#[test]
@@ -523,7 +532,7 @@ mod tests {
523532
allowed_algorithms: vec![JwtAlgorithm::RS256, JwtAlgorithm::ES256],
524533
..test_config()
525534
};
526-
let validator = JwtValidator::new(config);
535+
let validator = JwtValidator::new(config).unwrap();
527536

528537
let allowed = validator.allowed_algorithms();
529538
assert_eq!(allowed.len(), 2);

src/auth/oidc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ impl OidcAuthenticator {
247247
*validator = Some(Arc::new(JwtValidator::with_client(
248248
jwt_config,
249249
self.http_client.clone(),
250-
)));
250+
)?));
251251
}
252252
}
253253

src/middleware/layers/admin.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1036,7 +1036,7 @@ async fn validate_bearer_token(
10361036
};
10371037

10381038
let validator =
1039-
crate::auth::jwt::JwtValidator::with_client(jwt_config, state.http_client.clone());
1039+
crate::auth::jwt::JwtValidator::with_client(jwt_config, state.http_client.clone())?;
10401040

10411041
let claims = validator.validate(token).await?;
10421042

0 commit comments

Comments
 (0)