178 lines
6.6 KiB
Rust
178 lines
6.6 KiB
Rust
mod tests {
|
|
use super::*;
|
|
use crate::auth_provider::{
|
|
testing::{context, policy, spawn_mock},
|
|
Provider,
|
|
};
|
|
|
|
fn settings(url: &str) -> Settings {
|
|
Settings(forward_auth::Settings {
|
|
enabled: true,
|
|
url: url.into(),
|
|
allow_private: true,
|
|
..Default::default()
|
|
})
|
|
}
|
|
fn compile(url: &str) -> Compiled {
|
|
settings(url).compile(&policy(), &[]).unwrap().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_json_and_provider_path_contract_are_preserved() {
|
|
let parsed: Settings = serde_json::from_str(
|
|
r#"{"enabled":true,"url":"http://127.0.0.1:9300","allow_private":true}"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
serde_json::to_value(&parsed).unwrap()["url"],
|
|
"http://127.0.0.1:9300"
|
|
);
|
|
let auth = compile("http://127.0.0.1:9300");
|
|
assert_eq!(auth.id(), "authentik");
|
|
assert!(auth.reserved_path("/outpost.goauthentik.io"));
|
|
assert!(auth.reserved_path("/outpost.goauthentik.io/start"));
|
|
assert!(!auth.reserved_path("/outpost.goauthentik.io-evil"));
|
|
assert!(!auth.reserved_path("/other/outpost.goauthentik.io"));
|
|
}
|
|
#[tokio::test]
|
|
async fn unauthorized_redirect_uses_application_domain_not_portal_domain() {
|
|
let (url, server) = spawn_mock(
|
|
"401 Unauthorized",
|
|
"Set-Cookie: authentik_session=abc; Secure; Path=/; HttpOnly\r\n",
|
|
)
|
|
.await;
|
|
let auth = compile(&url);
|
|
let incoming = HeaderMap::new();
|
|
let response = auth
|
|
.authorize(
|
|
&incoming,
|
|
&Method::POST,
|
|
&context(&incoming, "app.other-domain.example"),
|
|
&CancellationToken::new(),
|
|
&CookieSink::default(),
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
assert_eq!(response.status(), StatusCode::FOUND);
|
|
let location = Url::parse(response.headers()[header::LOCATION].to_str().unwrap()).unwrap();
|
|
assert_eq!(
|
|
location.origin().ascii_serialization(),
|
|
"https://app.other-domain.example"
|
|
);
|
|
assert_eq!(location.path(), "/outpost.goauthentik.io/start");
|
|
assert_eq!(
|
|
location.query_pairs().find(|(k, _)| k == "rd").unwrap().1,
|
|
"https://app.other-domain.example/private?redirect=%2Fhome"
|
|
);
|
|
assert_eq!(
|
|
response.headers()[header::SET_COOKIE],
|
|
"authentik_session=abc; Secure; Path=/; HttpOnly"
|
|
);
|
|
server.await.unwrap();
|
|
}
|
|
#[tokio::test]
|
|
async fn forbidden_response_never_authorizes_backend() {
|
|
let (url, server) = spawn_mock("403 Forbidden", "").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::FORBIDDEN);
|
|
server.await.unwrap();
|
|
}
|
|
#[tokio::test]
|
|
async fn verified_headers_and_refresh_cookie_are_forwarded() {
|
|
let (url, server) = spawn_mock("200 OK", "X-Authentik-Username: allowed\r\nSet-Cookie: authentik_session=renew; Path=/; HttpOnly\r\n").await;
|
|
let auth = compile(&url);
|
|
let incoming = HeaderMap::new();
|
|
let cookies = CookieSink::default();
|
|
let identity = auth
|
|
.authorize(
|
|
&incoming,
|
|
&Method::GET,
|
|
&context(&incoming, "app.example.com"),
|
|
&CancellationToken::new(),
|
|
&cookies,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let mut headers = HeaderMap::new();
|
|
identity.apply(&mut headers);
|
|
assert_eq!(headers["x-authentik-username"], "allowed");
|
|
let mut response = Response::new(Body::empty());
|
|
cookies.apply(&mut response);
|
|
assert_eq!(
|
|
response.headers()[header::SET_COOKIE],
|
|
"authentik_session=renew; Path=/; HttpOnly"
|
|
);
|
|
server.await.unwrap();
|
|
}
|
|
#[tokio::test]
|
|
async fn outpost_is_proxied_only_on_reserved_path_with_trusted_host() {
|
|
let (url, server) = spawn_mock("302 Found", "Location: https://auth.example.com/if/flow/default/\r\nSet-Cookie: authentik=ok; Path=/\r\n").await;
|
|
let auth = compile(&url);
|
|
let incoming = HeaderMap::new();
|
|
let request = Request::builder()
|
|
.uri("/outpost.goauthentik.io/start?rd=https%3A%2F%2Fapp.example.com")
|
|
.header("X-Forwarded-Host", "spoofed.example")
|
|
.header("X-Authentik-Username", "forged")
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let response = auth
|
|
.serve_reserved(
|
|
request,
|
|
&context(&incoming, "app.example.com"),
|
|
&CancellationToken::new(),
|
|
)
|
|
.unwrap()
|
|
.await;
|
|
assert_eq!(response.status(), StatusCode::FOUND);
|
|
assert_eq!(
|
|
response.headers()[header::SET_COOKIE],
|
|
"authentik=ok; Path=/"
|
|
);
|
|
assert_eq!(
|
|
response.headers()[header::CACHE_CONTROL],
|
|
"private, no-store"
|
|
);
|
|
let request = server.await.unwrap().to_ascii_lowercase();
|
|
assert!(request.starts_with("get /outpost.goauthentik.io/start?rd="));
|
|
assert!(request.contains("host: app.example.com"));
|
|
assert!(request.contains("x-forwarded-host: app.example.com"));
|
|
assert!(!request.contains("spoofed.example"));
|
|
assert!(!request.contains("x-authentik-username: forged"));
|
|
}
|
|
#[tokio::test]
|
|
async fn auth_check_uses_provider_specific_endpoint() {
|
|
let (url, server) = spawn_mock("200 OK", "X-Authentik-Username: admin\r\n").await;
|
|
let auth = compile(&url);
|
|
let incoming = HeaderMap::new();
|
|
let identity = auth
|
|
.authorize(
|
|
&incoming,
|
|
&Method::GET,
|
|
&context(&incoming, "app.example.com"),
|
|
&CancellationToken::new(),
|
|
&CookieSink::default(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let mut headers = HeaderMap::new();
|
|
identity.apply(&mut headers);
|
|
assert_eq!(headers["x-authentik-username"], "admin");
|
|
assert!(server
|
|
.await
|
|
.unwrap()
|
|
.to_ascii_lowercase()
|
|
.contains("get /outpost.goauthentik.io/auth/nginx"));
|
|
}
|
|
}
|