Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion crates/config/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,82 @@
//!
//! Parsing of the Keystone configuration file implementation.
use serde::Deserialize;
use validator::Validate;

use crate::common::csv;

/// Authentication configuration.
#[derive(Debug, Default, Deserialize, Clone)]
#[derive(Debug, Deserialize, Clone, Validate)]
pub struct AuthProvider {
/// Authentication methods to be enabled and used for token validation.
#[serde(deserialize_with = "csv")]
pub methods: Vec<String>,

/// Maximum number of live entries of the TOTP passcode replay cache
/// (ADR 0044 §2) of the `local` store. When it is full new passcodes are
/// rejected rather than evicting older entries.
#[serde(default = "default_totp_replay_cache_max_entries")]
#[validate(range(min = 1))]
pub totp_replay_cache_max_entries: u32,

/// Maximum number of live TOTP replay entries held for one user.
#[serde(default = "default_totp_replay_cache_max_entries_per_user")]
#[validate(range(min = 1))]
pub totp_replay_cache_max_entries_per_user: u32,
}

fn default_totp_replay_cache_max_entries() -> u32 {
100_000
}

fn default_totp_replay_cache_max_entries_per_user() -> u32 {
32
}

impl Default for AuthProvider {
fn default() -> Self {
Self {
methods: Vec::new(),
totp_replay_cache_max_entries: default_totp_replay_cache_max_entries(),
totp_replay_cache_max_entries_per_user: default_totp_replay_cache_max_entries_per_user(
),
}
}
}

impl AuthProvider {
/// Check that the per-user TOTP replay bound does not exceed the total
/// (ADR 0044 §2).
pub fn validate_totp_replay(&self) -> Result<(), String> {
if self.totp_replay_cache_max_entries_per_user > self.totp_replay_cache_max_entries {
return Err(
"[auth] totp_replay_cache_max_entries_per_user must not exceed \
totp_replay_cache_max_entries"
.to_string(),
);
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_totp_replay_bounds() {
assert!(AuthProvider::default().validate_totp_replay().is_ok());
assert!(AuthProvider::default().validate().is_ok());
let bad = AuthProvider {
totp_replay_cache_max_entries: 10,
totp_replay_cache_max_entries_per_user: 11,
..Default::default()
};
assert!(bad.validate_totp_replay().is_err());
let zero = AuthProvider {
totp_replay_cache_max_entries: 0,
..Default::default()
};
assert!(zero.validate().is_err());
}
}
9 changes: 9 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ pub struct Config {
pub assignment: AssignmentProvider,

/// Authentication configuration.
#[validate(nested)]
pub auth: AuthProvider,

/// Catalog provider configuration.
Expand Down Expand Up @@ -396,6 +397,14 @@ impl Config {
.validate_dpop(cfg.mapping.cluster_salt.is_some())
.map_err(Report::msg)
.wrap_err("validating [oauth2] DPoP configuration")?;
cfg.oauth2
.validate_assertion_replay()
.map_err(Report::msg)
.wrap_err("validating [oauth2] assertion replay configuration")?;
cfg.auth
.validate_totp_replay()
.map_err(Report::msg)
.wrap_err("validating [auth] TOTP replay configuration")?;
Ok(cfg)
}

Expand Down
87 changes: 87 additions & 0 deletions crates/config/src/oauth2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,36 @@ pub struct Oauth2Provider {
#[serde(default = "default_dpop_replay_cache_max_entries_per_key")]
#[validate(range(min = 1))]
pub dpop_replay_cache_max_entries_per_key: u32,

/// Maximum number of live entries of the external assertion replay cache
/// (ADR 0044 §2) of the `local` store. When it is full new assertions are
/// rejected rather than evicting older entries.
#[serde(default = "default_assertion_replay_cache_max_entries")]
#[validate(range(min = 1))]
pub assertion_replay_cache_max_entries: u32,

/// Maximum number of live assertion replay entries held for one domain.
#[serde(default = "default_assertion_replay_cache_max_entries_per_domain")]
#[validate(range(min = 1))]
pub assertion_replay_cache_max_entries_per_domain: u32,

/// Maximum number of live assertion replay entries held for one issuer of
/// one domain.
#[serde(default = "default_assertion_replay_cache_max_entries_per_issuer")]
#[validate(range(min = 1))]
pub assertion_replay_cache_max_entries_per_issuer: u32,
}

fn default_assertion_replay_cache_max_entries() -> u32 {
100_000
}

fn default_assertion_replay_cache_max_entries_per_domain() -> u32 {
10_000
}

fn default_assertion_replay_cache_max_entries_per_issuer() -> u32 {
10_000
}

fn default_dpop_proof_lifetime_seconds() -> u32 {
Expand Down Expand Up @@ -547,6 +577,11 @@ impl Default for Oauth2Provider {
dpop_nonce_lifetime_seconds: default_dpop_nonce_lifetime_seconds(),
dpop_replay_cache_max_entries: default_dpop_replay_cache_max_entries(),
dpop_replay_cache_max_entries_per_key: default_dpop_replay_cache_max_entries_per_key(),
assertion_replay_cache_max_entries: default_assertion_replay_cache_max_entries(),
assertion_replay_cache_max_entries_per_domain:
default_assertion_replay_cache_max_entries_per_domain(),
assertion_replay_cache_max_entries_per_issuer:
default_assertion_replay_cache_max_entries_per_issuer(),
}
}
}
Expand Down Expand Up @@ -592,6 +627,30 @@ impl Oauth2Provider {
Ok(())
}

/// Check that the per-scope assertion replay bounds do not exceed their
/// totals (ADR 0044 §2): issuer within domain, domain within total.
pub fn validate_assertion_replay(&self) -> Result<(), String> {
if self.assertion_replay_cache_max_entries_per_domain
> self.assertion_replay_cache_max_entries
{
return Err(
"[oauth2] assertion_replay_cache_max_entries_per_domain must not \
exceed assertion_replay_cache_max_entries"
.to_string(),
);
}
if self.assertion_replay_cache_max_entries_per_issuer
> self.assertion_replay_cache_max_entries_per_domain
{
return Err(
"[oauth2] assertion_replay_cache_max_entries_per_issuer must not \
exceed assertion_replay_cache_max_entries_per_domain"
.to_string(),
);
}
Ok(())
}

/// Whether requests for `domain_id` may be served, per `enabled_domains`.
pub fn domain_allowed(&self, domain_id: &str) -> bool {
self.enabled_domains
Expand Down Expand Up @@ -727,6 +786,34 @@ mod tests {
assert!(cfg.validate_dpop(false).is_err());
}

#[test]
fn test_validate_assertion_replay_bounds() {
assert!(
Oauth2Provider::default()
.validate_assertion_replay()
.is_ok()
);
let per_domain = Oauth2Provider {
assertion_replay_cache_max_entries: 10,
assertion_replay_cache_max_entries_per_domain: 11,
assertion_replay_cache_max_entries_per_issuer: 5,
..Default::default()
};
assert!(per_domain.validate_assertion_replay().is_err());
let per_issuer = Oauth2Provider {
assertion_replay_cache_max_entries: 10,
assertion_replay_cache_max_entries_per_domain: 5,
assertion_replay_cache_max_entries_per_issuer: 6,
..Default::default()
};
assert!(per_issuer.validate_assertion_replay().is_err());
let zero = Oauth2Provider {
assertion_replay_cache_max_entries: 0,
..Default::default()
};
assert!(zero.validate().is_err());
}

#[test]
fn test_validate_rejects_bad_dpop_windows() {
for cfg in [
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub mod role;
pub mod scim_realm;
pub mod scim_resource;
pub mod service_grant;
pub mod single_use;
pub mod spiffe_key;
pub mod token;
pub mod trust;
Expand Down
Loading
Loading