feat(DPoP): add verification + X-Forwarded-Host X-Forwarded-Proto

This commit is contained in:
Edouard Vanbelle
2026-08-08 15:08:35 +02:00
parent 514bbc35ab
commit 2e6789e506
12 changed files with 879 additions and 21 deletions
+59
View File
@@ -1448,6 +1448,55 @@ pub struct AuthConfig {
///
/// Env: `OXICLOUD_REQUIRE_VERIFIED_EMAIL` (default `false`).
pub require_verified_email: bool,
/// DPoP session-binding enforcement (RFC 9449). Bound sessions —
/// those created with a `dpop_jkt` supplied at login — carry a
/// browser-held keypair thumbprint; the middleware verifies a
/// per-request signed proof so that stealing the session cookie
/// alone is useless without the private key.
///
/// Modes (see `DpopMode` enum):
/// * `Off` (default) — middleware is a pass-through; no
/// verification even when a proof is present. Ship-safe
/// default while the client rollout catches up.
/// * `Opportunistic` — verify when a proof is present, reject
/// mismatches; skip when absent. Warn on
/// `dpop.header_missing_but_session_bound`. Rollout mode.
/// * `Required` — bound sessions MUST present a valid proof.
/// Unbound sessions (`dpop_jkt IS NULL` — app passwords,
/// Nextcloud clients, legacy) remain exempt at the
/// middleware level.
///
/// Env: `OXICLOUD_DPOP_MODE` in `{off,opportunistic,required}`
/// (default `off`).
pub dpop_mode: DpopMode,
}
/// DPoP session-binding enforcement mode. See `AuthConfig::dpop_mode`
/// and `docs/plan/dpop.md` for the rollout strategy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DpopMode {
/// Middleware pass-through — DPoP header is neither required nor
/// verified. Default: safe while clients roll out proof-signing.
#[default]
Off,
/// Verify when present, allow when absent. Bound sessions still
/// get a warning audit line when they arrive without a proof.
Opportunistic,
/// Bound sessions (`dpop_jkt IS NOT NULL`) MUST present a valid
/// proof or 401. Unbound sessions remain exempt.
Required,
}
impl DpopMode {
pub fn from_env_str(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"off" => Some(Self::Off),
"opportunistic" => Some(Self::Opportunistic),
"required" => Some(Self::Required),
_ => None,
}
}
}
/// Self-service auth method. Exposed as `AuthConfig::allowed_auth_methods`
@@ -1583,6 +1632,7 @@ impl Default for AuthConfig {
auth_policies: Vec::new(),
allowed_auth_methods: vec![AuthMethod::Password, AuthMethod::MagicLink],
require_verified_email: false,
dpop_mode: DpopMode::Off,
}
}
}
@@ -2978,6 +3028,15 @@ impl AppConfig {
}
}
if let Ok(v) = env::var("OXICLOUD_DPOP_MODE") {
match DpopMode::from_env_str(&v) {
Some(mode) => config.auth.dpop_mode = mode,
None => panic!(
"OXICLOUD_DPOP_MODE={v:?} — expected one of off / opportunistic / required"
),
}
}
if let Ok(v) = env::var("OXICLOUD_REQUIRE_VERIFIED_EMAIL") {
config.auth.require_verified_email = v.parse::<bool>().unwrap_or(false);
}