fix(auth): stop demo mode from overriding authentication

The demo router hardcoded AuthMode::Disabled, which caused /auth/config
and /auth/me to lie and let demo endpoints be reached without a session
whenever the fabro-demo=1 cookie was set. With the cookie set on a
GitHub-configured server, /login rendered "Paste your dev token" with
no input and no GitHub button because /auth/config returned empty
methods.

Have the demo router inherit the real AuthMode so demo mode is purely a
data-source toggle: authentication is identical regardless of the
cookie. Update the translate test that locked in the old bypass, add a
companion test for the authed happy path, and add a regression test
that /auth/config returns real methods under the demo cookie.

As defense in depth, the login page now renders an explicit "no
authentication method is configured" state when methods is empty
instead of the misleading dev-token prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-04-21 15:45:35 -04:00
parent 64e4239534
commit 710f9869f0
No known key found for this signature in database
6 changed files with 66 additions and 6 deletions

View file

@ -40,7 +40,9 @@ export default function AuthLogin({ loaderData }: any) {
<p className="mt-3 text-center text-sm/6 text-fg-3 text-pretty">
{hasGitHub
? "Authenticate with your GitHub account to continue."
: "Paste your dev token to continue."}
: hasDevToken
? "Paste your dev token to continue."
: "No authentication method is configured on this server."}
</p>
<div className="mt-6 space-y-4">
{hasGitHub ? (

View file

@ -547,7 +547,7 @@ mod tests {
}
#[tokio::test]
async fn full_router_routes_demo_cookie_to_demo_handler_without_credentials() {
async fn full_router_rejects_demo_cookie_without_credentials() {
let state = test_state();
let app = server::build_router_with_options(
state,
@ -568,7 +568,37 @@ mod tests {
.await
.unwrap();
assert_status!(response, StatusCode::OK).await;
assert_status!(response, StatusCode::UNAUTHORIZED).await;
}
#[tokio::test]
async fn full_router_routes_demo_cookie_to_demo_handler_with_session() {
let state = test_state();
let cookie = session_cookie(&github_session(), state.as_ref());
let app = server::build_router_with_options(
Arc::clone(&state),
&auth_mode(),
Arc::new(crate::ip_allowlist::IpAllowlistConfig::default()),
RouterOptions::default(),
);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/health/diagnostics")
.header(header::COOKIE, format!("{cookie}; fabro-demo=1"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let body = response_json!(response).await;
assert_eq!(
body["sections"][0]["checks"][0]["summary"], "demo configured",
"demo handler should have served /health/diagnostics",
);
}
#[tokio::test]

View file

@ -975,7 +975,7 @@ pub fn build_router_with_options(
let demo_router = Router::new()
.nest("/api/v1", api_common.clone().merge(demo_routes()))
.layer(axum::Extension(AuthMode::Disabled))
.layer(axum::Extension(auth_mode.clone()))
.layer(axum::Extension(Arc::clone(&github_endpoints)))
.with_state(state.clone());

View file

@ -1117,6 +1117,34 @@ mod tests {
assert_eq!(body, json!({ "methods": ["dev-token"] }));
}
#[tokio::test]
async fn auth_config_returns_real_methods_when_demo_cookie_set() {
let state = server::create_test_app_state_with_session_key(
github_settings("https://fabro.example"),
Some("web-auth-test-key-material-0123456789"),
false,
);
let app = server::build_router_with_options(
state,
&github_auth_mode(),
Arc::new(crate::ip_allowlist::IpAllowlistConfig::default()),
server::RouterOptions::default(),
);
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/auth/config")
.header(header::COOKIE, "fabro-demo=1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let body = response_json!(response).await;
assert_eq!(body, json!({ "methods": ["github"] }));
}
#[tokio::test]
async fn login_github_sets_secure_state_cookie_for_https_web_url() {
let app = test_auth_router_with_settings(

View file

@ -58,7 +58,7 @@
<script type="module" src="/assets/chunk-sadshphz.js"></script>
<script type="module" src="/assets/chunk-pmthkscp.js"></script>
<script type="module" src="/assets/chunk-v61ks9f7.js"></script>
<script type="module" src="/assets/entry-nabtzckk.js"></script>
<script type="module" src="/assets/entry-q11nrnd3.js"></script>
<script type="module" src="/assets/chunk-n1k68xa8.js"></script>
<script type="module" src="/assets/chunk-rsph5pvm.js"></script>
<script type="module" src="/assets/chunk-9t57pdty.js"></script>