add swarm-authelia-bridge: the only thing allowed to write swarm-authelia's users database
This commit is contained in:
parent
16d578e692
commit
fb5d461e52
10 changed files with 876 additions and 0 deletions
80
swarm-authelia-bridge/src/introspect.rs
Normal file
80
swarm-authelia-bridge/src/introspect.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
//! Validating a presented bearer token against authelia.
|
||||
//!
|
||||
//! Own copy of `swarm-nats-auth::introspect`'s shape (RFC 7662 token
|
||||
//! introspection), not a shared dependency — `swarm-nats-auth` has no lib
|
||||
//! target to import, and this is a handful of lines; factor out if a third
|
||||
//! consumer shows up. Same verdict rule: `active` is the whole answer, and
|
||||
//! everything that isn't an explicit `active: true` is a denial (network
|
||||
//! error, timeout, non-2xx, unparseable body) — the failure modes of an
|
||||
//! HTTP call are exactly the conditions an attacker would like this to
|
||||
//! fall open under.
|
||||
//!
|
||||
//! Authenticates the introspection call itself with **this bridge's own**
|
||||
//! client credentials (a resource server introspecting a token proves its
|
||||
//! own identity to the `IdP`, per RFC 7662) — a separate authelia machine
|
||||
//! client from the token being checked (`swarm-controller`'s).
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Generous relative to `swarm-nats-auth`'s 1.5s (that one is bounded by
|
||||
/// the NATS server's own 2s `auth_callout` timeout; an HTTP request here
|
||||
/// has no such external deadline to stay under), but still bounded — a
|
||||
/// hung introspection call must not wedge a `CreateIdentity` job forever.
|
||||
pub const INTROSPECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct IntrospectionResponse {
|
||||
active: bool,
|
||||
}
|
||||
|
||||
/// Ask authelia whether `token` is currently valid. `Ok(true)` only on a
|
||||
/// 2xx body with `active: true`; every other outcome is `Ok(false)`
|
||||
/// (logged) or `Err` when the call could not be made at all — callers
|
||||
/// must treat both as a denial.
|
||||
pub async fn is_active(
|
||||
http: &reqwest::Client,
|
||||
url: &str,
|
||||
client_id: &str,
|
||||
client_secret: &str,
|
||||
token: &str,
|
||||
) -> Result<bool> {
|
||||
let resp = http
|
||||
.post(url)
|
||||
.basic_auth(client_id, Some(client_secret))
|
||||
.form(&[("token", token)])
|
||||
.timeout(INTROSPECTION_TIMEOUT)
|
||||
.send()
|
||||
.await
|
||||
.context("introspection request")?;
|
||||
|
||||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
tracing::warn!(%status, "introspection returned non-2xx; denying");
|
||||
return Ok(false);
|
||||
}
|
||||
let body: IntrospectionResponse = match resp.json().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "introspection body did not parse; denying");
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
Ok(body.active)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_active_true_deserializes_to_a_grant() {
|
||||
let yes: IntrospectionResponse = serde_json::from_str(r#"{"active":true}"#).unwrap();
|
||||
assert!(yes.active);
|
||||
let no: IntrospectionResponse = serde_json::from_str(r#"{"active":false}"#).unwrap();
|
||||
assert!(!no.active);
|
||||
assert!(serde_json::from_str::<IntrospectionResponse>(r#"{"sub":"someone"}"#).is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue