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")); } }