v0.34.15
This commit is contained in:
1 parent
62b7574553
commit
e364392659
47 files changed
+2164
-257
No files matched your search
@@ -0,0 +1,127 @@
|
||||
use crate::{
|
||||
auth_provider::{self, CookieSink},
|
||||
config::ValidationPolicy,
|
||||
forward_auth, forwarding,
|
||||
};
|
||||
use anyhow::{ensure, Result};
|
||||
use axum::response::Response;
|
||||
use http::{header, HeaderMap, Method, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Settings(forward_auth::Settings);
|
||||
impl Deref for Settings {
|
||||
type Target = forward_auth::Settings;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl DerefMut for Settings {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
self.0.validate::<Adapter>()
|
||||
}
|
||||
pub fn compile(
|
||||
&self,
|
||||
policy: &ValidationPolicy,
|
||||
bindings: &[SocketAddr],
|
||||
) -> Result<Option<Compiled>> {
|
||||
Ok(self.0.compile::<Adapter>(policy, bindings)?.map(Compiled))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Compiled(forward_auth::Compiled<Adapter>);
|
||||
#[derive(Debug)]
|
||||
pub struct Adapter;
|
||||
impl forward_auth::Adapter for Adapter {
|
||||
fn label() -> &'static str {
|
||||
"Authelia"
|
||||
}
|
||||
fn verification_path() -> &'static str {
|
||||
"/api/authz/forward-auth"
|
||||
}
|
||||
fn portal(settings: &forward_auth::Settings) -> Result<Option<Url>> {
|
||||
let portal = forward_auth::http_url(&settings.login_url, "Authelia login")?;
|
||||
ensure!(
|
||||
forward_auth::is_portal_https(&portal),
|
||||
"public Authelia login must use HTTPS"
|
||||
);
|
||||
Ok(Some(portal))
|
||||
}
|
||||
fn denied(
|
||||
status: StatusCode,
|
||||
response_headers: &HeaderMap,
|
||||
_original_url: &str,
|
||||
portal: Option<&Url>,
|
||||
redirect_forbidden: bool,
|
||||
) -> std::result::Result<Response, Response> {
|
||||
if matches!(
|
||||
status,
|
||||
StatusCode::FOUND | StatusCode::SEE_OTHER | StatusCode::UNAUTHORIZED
|
||||
) || (status == StatusCode::FORBIDDEN && redirect_forbidden)
|
||||
{
|
||||
let location = portal.and_then(|url| {
|
||||
forward_auth::login_redirect(response_headers.get(header::LOCATION), url)
|
||||
});
|
||||
if let Some(location) = location {
|
||||
let mut response = forward_auth::fail(if status == StatusCode::SEE_OTHER {
|
||||
StatusCode::SEE_OTHER
|
||||
} else {
|
||||
StatusCode::FOUND
|
||||
});
|
||||
response.headers_mut().insert(header::LOCATION, location);
|
||||
Ok(response)
|
||||
} else if matches!(status, StatusCode::FOUND | StatusCode::SEE_OTHER) {
|
||||
Err(forward_auth::invalid(
|
||||
"Authelia",
|
||||
"Authelia returned an untrusted login redirect",
|
||||
))
|
||||
} else {
|
||||
Ok(forward_auth::fail(status))
|
||||
}
|
||||
} else if status == StatusCode::FORBIDDEN {
|
||||
Ok(forward_auth::fail(status))
|
||||
} else {
|
||||
Err(forward_auth::invalid(
|
||||
"Authelia",
|
||||
"Authelia returned an unexpected verifier status",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl auth_provider::Provider for Compiled {
|
||||
fn id(&self) -> &'static str {
|
||||
"authelia"
|
||||
}
|
||||
fn strip_identity(&self, headers: &mut HeaderMap) {
|
||||
self.0.strip_identity(headers);
|
||||
}
|
||||
fn authorize<'a>(
|
||||
&'a self,
|
||||
incoming: &'a HeaderMap,
|
||||
method: &'a Method,
|
||||
context: &'a forwarding::Context,
|
||||
shutdown: &'a CancellationToken,
|
||||
cookies: &'a CookieSink,
|
||||
) -> auth_provider::Authorization<'a> {
|
||||
Box::pin(
|
||||
self.0
|
||||
.authorize(incoming, method, context, shutdown, cookies),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
include!("tests.rs");
|
||||
@@ -0,0 +1,85 @@
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::auth_provider::{Provider, testing::{context, policy, spawn_mock}};
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
fn settings(url: &str) -> Settings {
|
||||
let mut settings = Settings::default();
|
||||
settings.enabled = true;
|
||||
settings.url = url.into();
|
||||
settings.login_url = "https://auth.example.com".into();
|
||||
settings.allow_private = true;
|
||||
settings
|
||||
}
|
||||
fn compile(url: &str) -> Compiled { settings(url).compile(&policy(), &[]).unwrap().unwrap() }
|
||||
|
||||
#[test]
|
||||
fn legacy_json_shape_and_provider_identity_are_preserved() {
|
||||
let parsed: Settings = serde_json::from_str(r#"{"enabled":true,"url":"http://127.0.0.1:9300","login_url":"https://auth.example.com","allow_private":true}"#).unwrap();
|
||||
assert!(parsed.enabled);
|
||||
let serialized = serde_json::to_value(&parsed).unwrap();
|
||||
assert_eq!(serialized["url"], "http://127.0.0.1:9300");
|
||||
assert!(serialized.get("0").is_none());
|
||||
assert_eq!(compile("http://127.0.0.1:9300").id(), "authelia");
|
||||
}
|
||||
#[test]
|
||||
fn invalid_portal_and_cross_origin_redirects_are_rejected() {
|
||||
let mut s = settings("http://127.0.0.1:9300");
|
||||
s.login_url = "http://auth.example.com".into();
|
||||
assert!(s.validate().is_err());
|
||||
let portal = Url::parse("https://auth.example.com/").unwrap();
|
||||
for bad in ["https://evil.example/", "//evil.example", "https://auth.example.com@evil.example/", "javascript:alert(1)", "https://auth.example.com/#frag"] {
|
||||
assert!(forward_auth::login_redirect(Some(&bad.parse().unwrap()), &portal).is_none(), "{bad}");
|
||||
}
|
||||
assert_eq!(forward_auth::login_redirect(Some(&"/login?rd=1".parse().unwrap()), &portal).unwrap(), "https://auth.example.com/login?rd=1");
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn checks_forward_auth_endpoint_and_returns_only_verified_identity() {
|
||||
let (url, server) = spawn_mock("200 OK", "Remote-User: trusted\r\nRemote-Email: trusted@example.com\r\n").await;
|
||||
let auth = compile(&url);
|
||||
let mut incoming = HeaderMap::new();
|
||||
incoming.insert("remote-user", "forged".parse().unwrap());
|
||||
let identity = auth.authorize(&incoming, &Method::GET, &context(&incoming, "app.example.com"),
|
||||
&CancellationToken::new(), &CookieSink::default()).await.unwrap();
|
||||
let request = server.await.unwrap().to_ascii_lowercase();
|
||||
assert!(request.contains("get /api/authz/forward-auth"));
|
||||
assert!(request.contains("x-forwarded-host: app.example.com"));
|
||||
assert!(request.contains("x-forwarded-uri: /private?redirect=%2fhome"));
|
||||
assert!(!request.contains("remote-user: forged"));
|
||||
let mut headers = HeaderMap::new();
|
||||
identity.apply(&mut headers);
|
||||
assert_eq!(headers["remote-user"], "trusted");
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn redirects_only_to_trusted_portal_and_denies_untrusted_redirects() {
|
||||
let (url, server) = spawn_mock("302 Found", "Location: https://auth.example.com/?rd=home\r\n").await;
|
||||
let auth = compile(&url);
|
||||
let incoming = HeaderMap::new();
|
||||
let response = auth.authorize(&incoming, &Method::GET, &context(&incoming, "app.example.com"),
|
||||
&CancellationToken::new(), &CookieSink::default()).await.unwrap_err();
|
||||
assert_eq!(response.status(), StatusCode::FOUND);
|
||||
assert!(response.headers()[header::LOCATION].to_str().unwrap().starts_with("https://auth.example.com/"));
|
||||
server.await.unwrap();
|
||||
|
||||
let (url, server) = spawn_mock("302 Found", "Location: https://evil.example/\r\n").await;
|
||||
let auth = compile(&url);
|
||||
let response = auth.authorize(&incoming, &Method::GET, &context(&incoming, "app.example.com"),
|
||||
&CancellationToken::new(), &CookieSink::default()).await.unwrap_err();
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
server.await.unwrap();
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn unreachable_provider_fails_closed_with_no_origin_details() {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let url = format!("http://{}", listener.local_addr().unwrap());
|
||||
drop(listener);
|
||||
let auth = compile(&url);
|
||||
let incoming = HeaderMap::new();
|
||||
let response = auth.authorize(&incoming, &Method::GET, &context(&incoming, "app.example.com"),
|
||||
&CancellationToken::new(), &CookieSink::default()).await.unwrap_err();
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
assert!(response.extensions().get::<auth_provider::AuthenticationResponse>().is_some());
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
assert!(!body.windows(url.len()).any(|chunk| chunk == url.as_bytes()));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user